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

时间:2019-05-30 19:55:10

标签: c++ multithreading synchronization race-condition condition-variable

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

但是,等待谓词如何解决“丢失的唤醒”问题?如下面的代码所示; 5秒钟没有调用“ wait”,我期望它会丢失前几条通知;但是只要有早,它就不会错过任何东西。是否保存了这些通知以备将来使用?

#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 个答案:

答案 0 :(得分:1)

这是原子的“解锁并等待”操作,可以防止丢失唤醒。唤醒丢失的方式是这样的:

  1. 我们获得了保护数据的锁。
  2. 我们进行检查以查看是否需要等待,然后我们就知道了。
  3. 我们需要释放锁,因为否则其他线程将无法访问数据。
  4. 我们等待唤醒。

您可以在此处看到丢失唤醒的风险。在第3步和第4步之间,另一个线程可以获取锁并发送唤醒信息。我们已经释放了锁,所以另一个线程可以执行此操作,但是我们尚未等待,因此我们将无法获得信号。

只要在锁的保护下完成了步骤2,并且步骤3和4是原子的,就不会有丢失唤醒的风险。除非修改数据,否则无法发送唤醒;直到另一个线程获得了锁,才能进行唤醒。由于3和4是原子的,因此任何将锁视为未锁定的线程也必将看到我们在等待。

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

  

在上面的代码中,消费者因为正在休眠而不等待前几条通知。在这种情况下是否不缺少通知?这种情况与#3和#4之间的比赛情况不同吗?

不。不可能发生。

未等待的使用者持有该锁或不持有该锁。如果不等待的消费者持有该锁,它将不会错过任何东西。谓词持有锁时无法更改。

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

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

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