C++ 提供了condition_variable条件变量。标准使用模式是要配合mutexpredicate使用。

核心要点:

  • 等待方:拿着 unique_lock<mutex>,用 cv.wait(lock, predicate) 等待
  • 通知方:先修改共享状态(同一把 mutex 保护),再 notify_one/notify_all
  • 永远用 predicate 或 while 循环防止“虚假唤醒”(spurious wakeup)

标准使用模式

条件变量可以解决经典的 生产-消费 模型

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
#include <iostream>

std::mutex m;
std::condition_variable cv;
std::queue<int> q;
bool done = false; // 共享状态:是否结束

// 消费者线程
void consumer() {
while (true) {
std::unique_lock<std::mutex> lk(m);

// 等到:队列非空 或 done 为 true
cv.wait(lk, [] { return !q.empty() || done; });

if (done && q.empty()) {
break; // 收尾:done 且没有任务了就退出
}

int x = q.front();
q.pop();

lk.unlock(); // 处理任务前解锁,减少锁占用时间(常见优化)
std::cout << "consume " << x << "\n";
}
}

// 生产者线程
void producer() {
for (int i = 0; i < 5; i++) {
{
std::lock_guard<std::mutex> lk(m);
q.push(i);
}
cv.notify_one(); // 通知有新数据了
}

{
std::lock_guard<std::mutex> lk(m);
done = true;
}
cv.notify_all(); // 通知所有等待者退出
}

int main() {
std::thread t1(consumer);
std::thread t2(producer);

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

wait 函数

在调用wait函数之前,必须要把mutex上锁,否则无法执行后续的释放锁操作。

例如,把设置智能锁的操作改成std::unique_lock ulock(mt,std::defer_lock);,这个有概率会拿到没有上锁的锁,进而导致在wait中报错。

wait 会做三件事(原子地配合条件变量):

  1. 检查条件(后面的 lambda/predicate)
  2. 如果条件不满足:
    • 释放互斥锁** ****lk**
    • 让线程睡眠等待通知notify_one/notify_all 或者虚假唤醒)
  3. 被唤醒后:
    • 重新加锁 **lk**
    • 再次检查条件
    • 条件满足才返回,否则继续睡

wait函数的原型有

1
2
void wait(unique_lock<mutex>& _Lck) noexcept
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)

常用第二个,第二个的实现方式就是:

1
2
3
4
5
6
template <class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) { // wait for signal and test predicate
while (!_Pred()) {
wait(_Lck);
}
}

给定一个条件函数,防止出现虚假唤醒。

wait函数还有其他类型,可以设置等待时间的,其用法和mutex的一致。

notify

唤醒函数有两种:

  • notify_one随机唤醒一个线程
  • notify_all唤醒所有线程

唤醒是指唤醒哪些调用wait函数后挂起的线程,具体使用哪个函数要根据代码设计。一般notify_one适合产生了一个任务需要处理,而notify_all适合当全部线程需要更改状态的情况。

在上面的代码中,我们将lock_guard放在一对大括号内,这里使用了RAII机制,离开大括号自动解除锁,让调用notify_one之前解锁互斥锁, 即在线程觉醒前解锁互斥,算是微小的性能改进。

需要注意的是,ontify_one 在操作系统上并不保证一定只唤醒一个线程,也有可能多个,这个就是伪唤醒

伪唤醒

有些情况下,如果线程重新获得互斥,并且查验条件,而这一行为却不是直接响应线程乙的通知,这个就是伪唤醒

根据C++标准,这种伪唤醒出现的数量和频率都是不确定的,每次唤醒都会执行一次判断函数来检验条件,若这个判断函数有其他的副作用,那么不建议选择它来作为判断条件。(例如判断函数会提高线程优先级,多次意外唤醒会导致线程的优先级非常高)

避免伪唤醒的办法就是添加测试循环,也就是上面wait函数第二种定义,内部实现就含有测试循环。

唤醒丢失

首先我们不能将条件变量的notify_one当成一个事件或者信号。

唤醒丢失是在生产者线程notify_one时,消费者线程还没有进入wait函数进行等待,这个就会造成唤醒丢失。

下面代码就会造成唤醒丢失:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void worker() {
printf("I started\n");
std::this_thread::sleep_for(std::chrono::seconds(10));
std::unique_lock lock(mt);
cv.wait(lock);
printf("I am wake up\n");
}
void maker() {
printf("I notify_one\n");
cv.notify_one();
}

int main() {
std::jthread t1(maker);
std::jthread t2(worker);
return 0;
}

这种情况很难避免,但是我们可以通过一个谓词来进行判断生产者是否完成了生产。

wait那句改成 cv.wait(lock, [&]() {return done; });即可。这样即使在worker在等待的10秒中出现了唤醒丢失,程序执行到wait时发现条件符合也会直接获取到锁。