再理解 std::condition_variable 条件变量
有如下几个问题需要厘清:
- 工作原理,即流程;
- 虚假唤醒与唤醒丢失;
notify_one与notify_all。lock_guard与unique_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. 工作原理及流程
即,对producer,consumer两者需要确保互锁,并且producer进行notify的时候,consumer已经进入等待状态。
如果不使用带谓词的wait,则会出现唤醒丢失的问题,即producer发送notify的时候,consumer还没有进入等待状态,原因就是需要等待lock:
1
2
std::unique_lock<std::mutex> lck{mutex_};
condVar.wait(lck);
并且consumer进入等待状态之后,producer、consumer会进入死锁状态。
另一方面,如果不使用原子变量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 进行授权