为什么“用谓词等待”可以解决条件变量的“丢失唤醒”?

spa*_*spa 3 c++ multithreading synchronization condition-variable race-condition

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

但是wait with predicate 如何解决“丢失唤醒”的问题呢?正如你在下面的代码中看到的;'wait' 没有被调用 5 秒,我原以为它会错过前几个通知;但有了 predate,它不会错过任何一个。这些通知是否已保存以备将来等待?

#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;
}
Run Code Online (Sandbox Code Playgroud)

Dav*_*rtz 7

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

  1. 我们获取保护数据的锁。
  2. 我们检查是否需要等待,我们看到我们需要等待。
  3. 我们需要释放锁,否则没有其他线程可以访问数据。
  4. 我们等待醒来。

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

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

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

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

不。不可能发生。

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

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

因此,如果谓词使得线程不需要等待,则线程将不会等待,因为它会检查谓词。在步骤 1 之前没有机会错过唤醒。

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