C++提供了两个条件变量的实现:std::condition_variablestd::condition_variable_any,这两个都在 <condition_variable> 库的头文件中声明。两者都需要和互斥元一起工作,前者仅限于 std::mutex,后者可以与符合称为类似互斥元的最低标准的任何东西一起工作,因此跟普遍,所以会有大小、性能或者操作系统资源方面的形式的额外代价的可能。

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
std::mutex mut;
std::queue<data_chunk> data_queue; // 两个线程直接传递数据的队列
std::condition_variable data_cond;

void data_preparetion_thread()
{
while (more_data_to_prepare())
{
data_chunk const data = prepare_data();
std::lock_guard<std::mutex> lk(mut); // 锁定保护队列的互斥元
{
data_queue.push(data);
}
data_cond.notify_one(); // 通知等待中的线程(如果有)
}
}

void data_processing_thread()
{
while (true)
{
std::unique_lock<std::mutex> lk(mut); // 锁定
data_cond.wait(lk, []{ return !data_queue.empty(); }); // 等待
{
data_chunk data = data_queue.front();
data_queue.pop();
}
lk.unlock(); // 解锁

process(data);
if (is_last_chunk(data))
break;
}
}

当来自数据准备线程中对 notify_one() 的调用通知条件变量时,线程从睡眠状态中苏醒(解除其阻塞),重新获得互斥元上的锁,并再次检查条件,如果条件已经满足,就从 wait() 返回值,互斥元仍被锁定;如果条件不满足,该线程解锁互斥元,并恢复等待。

这就是为什么需要 std::unique_lock 而不是 std::lock_guard ——等待中的线程在等待期间必须解锁互斥元,并在这之后重新将其锁定,而 std::guard_lock 没有提供这样的灵活性。

在对 wait() 的调用中,条件变量可能会对所提供的条件检查任意多次。然而,它总是在互斥元被锁定的情况下这样做,并且当(且仅当)用来测试条件的函数返回 true,它就会立即返回。

当等待线程重新获取互斥元并检查条件时,如果它并非直接响应另一个线程的通知,这就是所谓的伪唤醒。由于所有的这种伪唤醒的次数和频率根据根据定义是不确定的,所以使用对于条件检查具有副作用的函数是不可取的。如果你这样做,就必须做好多次产生副作用的准备。