文章

再理解 std::condition_variable 条件变量

有如下几个问题需要厘清:

  • 工作原理,即流程;
  • 虚假唤醒与唤醒丢失;
  • notify_onenotify_all
  • lock_guardunique_lock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <atomic>
#include <condition_variable>
#include <iostream>
#include <thread>

std::mutex mutex_;
std::condition_variable condVar;
std::atomic<bool> dataReady{false};       // (0)

void waitingForWork() {
    std::cout << "Waiting " << std::endl;
    std::unique_lock<std::mutex> lck(mutex_);
    condVar.wait(lck, []{ return dataReady.load(); });   // (1)
    std::cout << "Running " << std::endl;
    // do the work
}

void setDataReady() {
    {
         std::lock_guard<std::mutex> lck(mutex_);       // (2)
         // prepare the data
         dataReady = true;
    }
    std::cout << "Data prepared" << std::endl;
    condVar.notify_one();                               // (3)
}

int main(){
  std::thread t1(waitingForWork);
  std::thread t2(setDataReady);

  t1.join();
  t2.join();
  
  return 0;
}

其中,带谓词(predicate)wait等效于:

1
2
3
4
5
std::unique_lock<std::mutex> lck{mutex_};   // (a)
while(![]{return dataReady.load();}) {      // (b)
    //time window(1)
    condVar.wait(lck);                      // (c)
}

consumer获取到锁之后:

  • 如果谓词返回false,则通过wait操作释放锁(调用lck.unlock()),并进入等待状态,直到notify唤醒。
  • 被唤醒之后,重新获取锁(调用lck.lock()),进入时间窗口(1),再次检查谓词
  • 如果谓词返回true,则继续持锁继续后续的工作,直到释放锁。

参见std::condition_variable::wait – 中文版

1. 工作原理及流程

即,对producerconsumer两者需要确保互锁,并且producer进行notify的时候,consumer已经进入等待状态。

如果不使用带谓词wait,则会出现唤醒丢失的问题,即producer发送notify的时候,consumer还没有进入等待状态,原因就是需要等待lock

1
2
std::unique_lock<std::mutex> lck{mutex_};
condVar.wait(lck);

并且consumer进入等待状态之后,producerconsumer会进入死锁状态。

另一方面,如果不使用原子变量dataReady,则会出现虚假唤醒的问题。即,如果dataReady是一个普通变量,可能由于缓存不一致性,导致consumer读取到的值不正确,如果读取到的值为true,则出现虚假唤醒

2. notify_one 与 notify_all

当使用notify_all的时候,后续被唤醒的consumer需要等第一个consumer释放锁之后,才能重新获取锁,然后再经过一次谓词检查,此时,可能谓词返回false,则继续进入等待状态。

3. lock_guard 与 unique_lock

对于producer,使用lock_guard即可,因为只需要持锁一次操作完成之后随即释放,没有再次持锁的需求。也可以使用``unique_lock,但需要保证在notify`之前释放锁。

对于consumer,必须使用unique_lock,因为wait需要传入一个unique_lock对象,并且在wait过程中会释放锁,等待被唤醒之后重新获取锁继续工作。

参考

本文由作者按照 CC BY 4.0 进行授权