为啥“等待谓词”解决了条件变量的“丢失唤醒”?

Posted

技术标签:

【中文标题】为啥“等待谓词”解决了条件变量的“丢失唤醒”?【英文标题】:Why 'wait with predicate' solves the 'lost wakeup' for condition variable?为什么“等待谓词”解决了条件变量的“丢失唤醒”? 【发布时间】:2019-05-30 19:55:10 【问题描述】:

我试图了解在条件变量的情况下虚假唤醒与丢失唤醒之间的区别。以下是我尝试过的一小段代码。我知道在这种情况下,“消费者”可能会在没有任何通知的情况下醒来,因此等待需要检查谓词。

但是等待谓词如何解决“丢失唤醒”的问题?正如您在下面的代码中看到的那样; 'wait' 没有被调用 5 秒,我预计它会错过前几个通知;但早于,它不会错过任何。这些通知是否已保存以供将来等待?

#include <iostream>
#include <deque>
#include <condition_variable>
#include <thread>

std::deque<int> q;
std::mutex m;
std::condition_variable cv;

void dump_q()

    for (auto x: q) 
        std::cout << x << std::endl;
    


void producer()

    for(int i = 0; i < 10; i++) 
        std::unique_lock<std::mutex> locker(m);
        q.push_back(i);
        std::cout << "produced: " << i << std::endl;
        cv.notify_one();

        std::this_thread::sleep_for(std::chrono::seconds(1));
        locker.unlock();
    


void consumer()

    while (true) 
        int data = 0;
        std::this_thread::sleep_for(std::chrono::seconds(5));   // <- should miss first 5 notications?
        std::unique_lock<std::mutex> locker(m); 
        cv.wait(locker);
        //cv.wait(locker, []()return !q.empty(););  // <- this fixes both spurious and lost wakeups
        data = q.front();
        q.pop_front();
        std::cout << "--> consumed: " << data << std::endl;
        locker.unlock();
    


int main(int argc, char *argv[])

    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;

【问题讨论】:

【参考方案1】:

这是防止丢失唤醒的原子“解锁和等待”操作。丢失的唤醒以这种方式发生:

    我们获取保护数据的锁。 我们检查是否需要等待,我们发现确实需要。 我们需要释放锁,否则没有其他线程可以访问数据。 我们等待唤醒。

您可以在此处查看丢失唤醒的风险。在步骤 3 和 4 之间,另一个线程可以获取锁并发送唤醒。我们已经释放了锁,所以另一个线程可以这样做,但我们还没有等待,所以我们不会收到信号。

只要步骤 2 在锁的保护下完成,并且步骤 3 和 4 是原子的,就没有丢失唤醒的风险。在修改数据之前无法发送唤醒,直到另一个线程获取锁才能完成。由于 3 和 4 是原子的,任何将锁视为未锁定的线程也必然会看到我们在等待。

这种原子的“解锁和等待”是条件变量的主要目的,也是它们必须始终与互斥锁和谓词相关联的原因。

在上面的代码中,消费者没有等待前几个通知,因为它正在休眠。在这种情况下是否没有丢失通知?这种情况与#3 和#4 之间的竞态条件不相似吗?

不。不可能发生。

没有等待的消费者要么持有锁,要么没有。如果没有等待的消费者持有锁,它就不会错过任何东西。谓词在持有锁时不能更改。

如果消费者没有持有锁,那么它错过了什么并不重要。当它检查是否应该在步骤 2 中锁定时,如果它错过了任何东西,它必然会在步骤 2 中看到它并且它会看到它不需要等待,因此它不会等待它错过的唤醒。

所以如果谓词是这样的,线程不需要等待,线程将不会等待,因为它检查谓词。在第 1 步之前没有机会错过唤醒。

真正需要唤醒的唯一时间是线程进入睡眠状态。原子解锁和睡眠确保线程只能在它持有锁并且它需要等待的事情尚未发生时决定进入睡眠状态。

【讨论】:

在上面的代码中,消费者没有等待前几个通知,因为它正在休眠。在这种情况下是否没有丢失通知?这种情况与#3 和#4 之间的竞争条件不相似吗?感谢您的帮助。 @spa 我会更新我的答案。在这种情况下,第 2 步不可能发生,所以我们永远不会进入第 4 步。 @DavidSchwartz,很抱歉这么晚才问这个问题。我仍然不确定一件事。你是说消费者肯定会错过通知(因为睡眠,std::this_thread::sleep_for(std::chrono::seconds(5));)?但是当消费者检查一个谓词时,它是否错过它们并不重要? @awakened 正确。如果它当时没有等待唤醒,它是否错过唤醒也没关系。

以上是关于为啥“等待谓词”解决了条件变量的“丢失唤醒”?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 autoconf 脚本中的条件在变量前加上“x”?

为啥这个 (null || !TryParse) 条件会导致“使用未分配的局部变量”?

Linux中 条件变量为啥要用互斥锁来保护?

为啥我无法将程序中找到的布尔结果的正确值分配给布尔变量并使用结果检查条件?

在导入Oracle数据库的时候违反唯一约束条件是为啥?要怎么解决?

尽管条件仍然成立,为啥这个 for 循环似乎没有执行?