condition_variable
std::condition_variable 用来解决这样一类问题:
某个条件现在还不满足,我不想一直占着 CPU 检查,而是希望先睡眠,等条件可能发生变化时再醒来。
典型场景包括:
队列里暂时没有任务
→ worker 先睡眠
初始化还没完成
→ 其他线程先等待
消费者暂时没有数据
→ 等生产者放入数据
多个线程等待某个状态变为 ready
→ 状态变化后再继续条件变量最重要的不是背:
wait()
notify_one()
notify_all()而是理解它必须围绕下面这套东西一起使用:
共享状态 + mutex + condition_variable + predicate它们的分工是:
共享状态
→ 记录“事情到底有没有发生”
mutex
→ 保护共享状态的读写
condition_variable
→ 条件不满足时让线程睡眠
→ 条件可能变化时把等待线程叫醒
predicate
→ 被唤醒以后重新判断:
“现在到底能不能继续执行?”所以:
condition_variable本身不保存业务状态,它只是一个“等待和唤醒机制”。
条件变量基础
为什么不能一直循环检查
最直接的等待方法可能是:
while (!ready)
{
}这种代码有两个问题。
首先,如果:
bool ready = false;是普通变量,而且多个线程同时读写它,那么可能产生:
data race即使改成:
std::atomic<bool> ready{false};然后:
while (!ready.load())
{
}虽然线程安全了,但线程会不断执行:
load
load
load
load
load
...这叫:
忙等(busy waiting)
也就是线程虽然什么有用的事都没干,却一直占着 CPU 检查条件。
如果可能要等几毫秒、几秒,甚至更久,这通常很浪费。
条件变量的思想是:
条件不满足
↓
线程睡眠,不占着 CPU 空转
其他线程修改状态
↓
发出通知
等待线程醒来
↓
重新检查条件所以条件变量解决的不是:
怎么让 bool 线程安全而是:
条件不满足时怎么高效等待
条件可能满足时怎么把线程叫醒条件变量最基本的三个对象
通常会看到:
std::mutex mutex;
std::condition_variable cv;
bool ready = false;这里:
ready是真正的共享状态,表示:
“现在能不能继续执行?”mutex:
std::mutex mutex;负责保护:
ready的读写。
cv:
std::condition_variable cv;负责:
让线程睡眠
+
把线程唤醒最重要的一点:
condition_variable本身不是状态。
也就是说,下面这种想法是错的:
“我收到 notify 了,所以条件肯定成立了。”真正决定线程能不能继续的是:
ready或者:
!queue.empty()或者其他业务状态。
最简单的 wait / notify 示例
#include <condition_variable>
#include <mutex>
#include <print>
#include <thread>
std::mutex mutex;
std::condition_variable cv;
bool ready = false;
void worker()
{
std::unique_lock lock(mutex);
cv.wait(lock, [] {
return ready;
});
std::println("worker start");
}
int main()
{
std::thread thread(worker);
{
std::lock_guard lock(mutex);
ready = true;
}
cv.notify_one();
thread.join();
}运行结果:
worker start整个过程可以理解成:
worker 获得 mutex
↓
检查 ready
↓
发现 ready == false
↓
wait():
释放 mutex
并让 worker 睡眠
↓
main 获得 mutex
↓
ready = true
↓
main 释放 mutex
↓
notify_one()
↓
worker 被唤醒
↓
worker 重新获得 mutex
↓
重新检查 ready
↓
ready == true
↓
wait() 返回
↓
打印 worker start这里最重要的一步是:
wait() 会在睡眠期间释放 mutex否则程序会死锁。
为什么 wait() 睡眠时必须释放 mutex
假设 worker 这样干:
拿到 mutex
↓
发现 ready == false
↓
拿着 mutex 睡觉那 main 想执行:
ready = true;之前也必须先:
lock(mutex);但 mutex 一直被 worker 拿着。
于是:
worker:
等 ready 变成 true
但一直拿着 mutex
main:
想把 ready 改成 true
但拿不到 mutex结果就是:
谁也进行不下去所以条件变量的 wait() 必须做一件非常特殊的事情:
当前已经持有 mutex
↓
原子地释放 mutex + 进入等待
↓
收到唤醒
↓
重新获得 mutex
↓
再返回为什么 wait() 要配合 std::unique_lock
常见写法:
std::unique_lock lock(mutex);
cv.wait(lock, predicate);而不是:
std::lock_guard lock(mutex);原因是:
wait() 需要暂时:
unlock()然后以后再:
lock()而 std::unique_lock 支持这种:
临时释放锁
↓
以后重新获得锁的所有权管理。
std::lock_guard 不支持手动解锁和重新加锁,所以不能满足普通 condition_variable::wait() 的要求。
可以把:
cv.wait(lock, predicate);粗略理解成:
while (!predicate())
{
cv.wait(lock);
}而内部的:
cv.wait(lock);会做:
释放 mutex
+
线程睡眠
+
被唤醒
+
重新获得 mutex为什么“释放 mutex + 睡眠”必须是原子的
这里还有一个很重要的问题。
假设 wait 的内部逻辑不是原子的,而是:
1. unlock mutex
2. 过一会儿才真正开始睡眠那么可能发生:
worker:
unlock mutex
↓
还没真正睡下去
main:
修改 ready = true
↓
notify_one()
worker:
此时才进入睡眠这样:
notify 已经发生了
worker 却刚刚开始睡worker 就可能一直睡下去。
所以条件变量必须保证:
“释放 mutex”和“进入等待”之间不会留下一个能丢通知的空档。
这也是为什么不要自己用:
unlock();
sleep();
lock();去模拟 condition_variable::wait()。
wait 与 predicate
wait(lock) 和 wait(lock, predicate)
条件变量有两种常见等待方式。
不带谓词
cv.wait(lock);它的意思是:
先睡眠
↓
被唤醒后重新拿锁
↓
返回问题是:
被唤醒并不代表你的业务条件一定成立。
所以如果用这种形式,一般必须自己写循环:
while (!ready)
{
cv.wait(lock);
}带谓词
推荐:
cv.wait(lock, [] {
return ready;
});它可以理解成标准库替你写了:
while (!ready)
{
cv.wait(lock);
}所以:
cv.wait(lock, predicate);真正表达的是:
只要 predicate 还不成立,我就继续等待。
例如:
cv.wait(lock, [] {
return !queue.empty();
});表示:
只要队列还是空的,我就继续等。
什么是 predicate
predicate 就是:
判断“现在能不能继续”的条件。
例如:
[] {
return ready;
}或者:
[] {
return !queue.empty();
}或者:
[] {
return !queue.empty() || finished;
}所以:
condition_variable
→ 负责把你叫醒
predicate
→ 决定你醒来以后到底能不能继续可以记成:
notify 只是说“起来看看”;predicate 才说“现在能不能走”。
虚假唤醒(spurious wakeup)
条件变量允许一种情况:
没有真正的业务事件发生
↓
wait() 却醒了这叫:
虚假唤醒(spurious wakeup)
所以这种写法不安全:
cv.wait(lock);
use_shared_data();因为:
wait() 返回并不一定意味着:
共享数据已经满足条件正确写法是:
cv.wait(lock, [] {
return ready;
});或者等价的:
while (!ready)
{
cv.wait(lock);
}也就是说:
每次醒来都必须重新检查真正的共享状态。
notify 与状态
notify 不是状态
条件变量还有一个非常容易误解的地方:
cv.notify_one();不是在:
condition_variable 里面存了一条消息它更像是:
“现在正在等的人,可以起来重新检查条件了。”
如果通知发生时:
根本没有线程在等待那么这次通知就结束了。
以后才来 wait 的线程:
不会补收到这次旧通知例如:
线程 A:
notify_one()
↓
当时没人等待
↓
通知结束
过了一会儿
线程 B:
开始 wait()线程 B 不会收到线程 A 之前那次通知。
这类现象通常称为:
丢失唤醒(lost wakeup)
那为什么不会轻易因为“通知先发生”而出错
因为正确的代码不会依赖:
“通知有没有发生过”而是依赖:
“共享状态现在是什么”例如:
{
std::lock_guard lock(mutex);
ready = true;
}
cv.notify_one();即使:
ready = true
notify_one()都发生在 worker 开始 wait 之前,也没问题。
因为 worker 后来执行:
cv.wait(lock, [] {
return ready;
});会先检查:
ready发现已经:
true于是:
根本不会睡所以:
共享状态
→ 保存事实
condition_variable
→ 只是优化等待方式这是条件变量最核心的理解之一。
notify_one()
cv.notify_one();表示:
唤醒一个当前正在等待这个 condition_variable 的线程。
例如有:
10 个 worker 都在等任务但现在只放入了:
1 个任务通常:
notify_one();就够了。
因为最终只有一个 worker 能消费这个任务。
notify_all()
cv.notify_all();表示:
唤醒所有当前正在等待这个 condition_variable 的线程。
适合:
程序准备退出
全局状态发生变化
所有 worker 都应该重新检查退出条件例如:
finished = true;
cv.notify_all();所有线程都会被叫醒。
但:
notify_all()不代表:
所有线程同时进入临界区它们醒来以后仍然要:
竞争同一把 mutex
↓
一个一个拿锁
↓
检查 predicate所以:
notify_all()
→ 所有人都醒来看看
不是
→ 所有人同时进入临界区修改状态以后什么时候 notify
很常见的写法是:
{
std::lock_guard lock(mutex);
ready = true;
}
cv.notify_one();顺序是:
持锁修改共享状态
↓
释放 mutex
↓
notify这样等待线程被叫醒以后:
更有机会直接拿到 mutex而不是:
刚醒
↓
发现通知线程还拿着 mutex
↓
又继续阻塞不过要注意:
notify()并不是规定“必须永远放在锁外”。
例如:
{
std::lock_guard lock(mutex);
ready = true;
cv.notify_one();
}在很多场景下也完全正确。
真正决定正确性的核心是:
共享谓词状态必须正确同步
wait 方必须在锁保护下检查 predicate把 notify 放锁外,更多是常见的性能和调度优化习惯。
生产者消费者
生产者消费者模型
这是条件变量最经典的应用。
假设:
producer
→ 不断往 queue 塞数据
consumer
→ 没数据时睡眠
→ 有数据时取出来处理同时还需要考虑:
producer 最终会结束所以消费者真正等待的条件不能只是:
!queue.empty()还要考虑:
finished完整示例:
#include <condition_variable>
#include <mutex>
#include <print>
#include <queue>
#include <thread>
std::queue<int> queue;
std::mutex mutex;
std::condition_variable cv;
bool finished = false;
void producer()
{
for (int value = 1; value <= 5; ++value)
{
{
std::lock_guard lock(mutex);
queue.push(value);
}
cv.notify_one();
}
{
std::lock_guard lock(mutex);
finished = true;
}
cv.notify_all();
}
void consumer()
{
while (true)
{
int value;
{
std::unique_lock lock(mutex);
cv.wait(lock, [] {
return !queue.empty() || finished;
});
if (queue.empty() && finished)
{
break;
}
value = queue.front();
queue.pop();
}
std::println("consume {}", value);
}
}
int main()
{
std::thread producer_thread(producer);
std::thread consumer_thread(consumer);
producer_thread.join();
consumer_thread.join();
}输出:
consume 1
consume 2
consume 3
consume 4
consume 5为什么 predicate 是:
!queue.empty() || finished而不是:
!queue.empty()假设生产者已经彻底结束:
finished == true同时队列也空了:
queue.empty() == true如果 consumer 只等待:
!queue.empty()那么:
以后永远不会再有数据
↓
consumer 却继续等待“队列非空”
↓
没人会再生产任务
↓
consumer 永远睡下去所以正确的等待条件应该表达:
有任务了
或者
生产已经结束了也就是:
!queue.empty() || finishedconsumer 醒来后为什么还要判断
等待结束以后:
cv.wait(lock, [] {
return !queue.empty() || finished;
});这里只说明:
队列非空
或者
生产结束至少有一个是真的。
所以接下来要区分:
情况:队列有数据
queue 非空
↓
取出一个任务情况:队列为空,而且 finished == true
queue.empty()
&&
finished说明:
现在没任务
而且以后也不会再有任务这时 consumer 才可以安全退出。
为什么真正处理任务要放到锁外
consumer 里面:
{
std::unique_lock lock(mutex);
// 检查 queue
// pop 一个任务
}
process_task();一般比:
std::unique_lock lock(mutex);
queue.pop();
process_task(); // 耗时 2 秒更好。
原因是:
如果你拿着 mutex 做耗时工作:
consumer 拿着 queue mutex 处理 2 秒那么这 2 秒内:
producer 不能 push
其他 consumer 不能 pop所有线程都被这一把锁堵住了。
所以锁内尽量只做:
检查共享状态
读取 / 修改共享状态
取出任务真正耗时的业务逻辑:
放到锁外这是多线程程序里非常重要的习惯。
超时与高级用法
wait_for()
可以让线程:
最多等一段时间。
例如:
bool ok = cv.wait_for(
lock,
std::chrono::seconds(1),
[] {
return ready;
});返回:
true
→ 在等待结束前 predicate 成立
false
→ 超时以后 predicate 仍然不成立例如:
if (!cv.wait_for(
lock,
std::chrono::seconds(1),
[] {
return ready;
}))
{
std::println("timeout");
}注意:
timeout不代表程序一定出错。
它只表示:
这段时间里条件没有成立至于超时以后:
重试
退出
打印日志
走备用逻辑由业务决定。
wait_until()
wait_until() 用来等待到某个:
绝对时间点例如:
auto deadline =
std::chrono::steady_clock::now()
+ std::chrono::seconds(2);
cv.wait_until(
lock,
deadline,
[] {
return ready;
});可以理解成:
一直等
直到:
ready == true
或者
到达 deadline如果只是:
等 2 秒通常:
wait_for(...)更直观。
如果你本来就有:
明确截止时间那么:
wait_until(...)更合适。
为什么超时通常推荐 steady_clock
涉及“持续多久”时:
std::chrono::steady_clock一般更合适。
因为它是单调时钟,不会因为:
用户手动改系统时间
NTP 校时
系统时间向前 / 向后跳而突然改变。
所以这种代码:
auto deadline =
std::chrono::steady_clock::now()
+ 2s;通常比基于墙上时间更适合:
超时
计时
等待一个 condition_variable 能等待多个条件吗
当然可以。
真正决定等待条件的是:
predicate例如:
cv.wait(lock, [] {
return !queue.empty()
|| stopped
|| error;
});表示:
只要下面任何一个发生:
队列有数据
或
程序停止
或
发生错误
就结束等待并继续处理然后醒来后再判断:
if (error)
{
// 处理错误
}
else if (stopped && queue.empty())
{
// 退出
}
else
{
// 处理队列任务
}所以:
condition_variable并不关心你到底在等几个条件。
它只负责:
睡
+
醒复杂条件都由:
predicate决定。
但 predicate 不要写得太乱
如果你开始写:
cv.wait(lock, [] {
return
a && b
|| c && !d
|| error_code != 0
|| queue.size() > 3
|| ...
});说明共享状态设计可能已经开始变复杂了。
这时候应该考虑:
能不能定义更清楚的状态对象
能不能使用 enum 表示状态
能不能拆分不同等待条件
能不能简化线程之间的协议而不是把所有业务逻辑全部堆进一个巨大 predicate。
condition_variable_any
标准库还有:
std::condition_variable_any普通:
std::condition_variable主要和:
std::unique_lock<std::mutex>配合。
而:
std::condition_variable_any可以配合更广泛的锁类型。
所以它:
更灵活但通常:
也可能有更多开销如果只是:
std::mutex普通场景优先:
std::condition_variable就够了。
condition_variable_any 和 stop_token
C++20 中:
std::condition_variable_any还有一个很实用的地方:
可以和
std::stop_token配合等待。
这对于:
std::jthread很方便。
例如 worker 正在:
等待队列任务同时程序又希望:
request_stop()以后能把它从等待中唤醒并退出。
这时:
condition_variable_any
+
stop_token就可以把:
业务条件成立
或者
收到停止请求组合进同一个等待过程里。
condition_variable 和 atomic::wait 怎么选
C++20 以后:
std::atomic也支持:
wait()
notify_one()
notify_all()如果只是:
等待一个 atomic 值变化例如:
std::atomic<bool> ready{false};
ready.wait(false);这种场景:
atomic::wait会非常方便。
如果等待条件是:
queue 非空
或者 finished == true
或者 error != 0这类涉及:
多个共享变量
容器
复杂业务状态通常:
mutex
+
condition_variable
+
predicate更加自然。
可以简单记成:
只围绕一个 atomic 值等待
→ atomic::wait()
围绕一组共享业务状态等待
→ condition_variable常见错误
错误:把 notify 当成状态
错误想法:
“notify 已经调用了,
所以以后 wait 一定知道。”不对。
cv.notify_one();不会永久保存一条消息。
真正的状态必须放在:
ready或者:
queue或者:
finished中。
错误:不用 predicate 处理虚假唤醒
不推荐:
cv.wait(lock);
use_data();应该:
cv.wait(lock, [] {
return ready;
});错误:等待方加锁,修改方却裸写共享变量
例如:
bool ready;worker:
std::unique_lock lock(mutex);
cv.wait(lock, [] {
return ready;
});main 却:
ready = true; // 没有锁这样仍然可能产生数据竞争。
对于这种普通共享状态:
读和写都应该遵守同一套 mutex 同步规则。
错误:拿着锁做耗时工作
错误:
std::unique_lock lock(mutex);
auto task = queue.front();
queue.pop();
do_heavy_work(task);更好:
Task task;
{
std::unique_lock lock(mutex);
task = queue.front();
queue.pop();
}
do_heavy_work(task);尽快释放锁。
错误:停止时只修改 finished,却忘了 notify
例如:
{
std::lock_guard lock(mutex);
finished = true;
}如果 worker 正在:
cv.wait(...)它可能仍然睡着。
通常还需要:
cv.notify_all();把等待线程叫醒,让它们重新检查:
finished错误:只考虑“现在没任务”,没考虑“以后也不会有任务”
生产者消费者模型必须设计:
什么时候表示生产彻底结束否则 consumer 很容易:
队列空
↓
继续 wait
↓
但 producer 已经退出
↓
以后永远没人再 notify所以通常需要:
bool finished;或者其他明确的结束状态。
最后总结
std::condition_variable 最核心的不是:
notify_one()而是这一整套关系:
共享状态
+
mutex
+
condition_variable
+
predicate可以这样记:
共享状态
→ 记录事实
mutex
→ 保护事实
condition_variable
→ 负责睡眠和唤醒
predicate
→ 决定醒来后到底能不能继续wait() 的核心流程:
拿着 mutex
↓
predicate 不成立
↓
释放 mutex + 睡眠
↓
收到 notify
↓
重新获得 mutex
↓
重新检查 predicate
↓
条件成立才真正返回另外几个重点:
notify_one()
→ 唤醒一个等待者
notify_all()
→ 唤醒所有等待者
wait_for()
→ 最多等一段时间
wait_until()
→ 最多等到某个时间点最后一定记住:
condition_variable 没有“记忆”,真正保存状态的是共享变量。
以及:
被 notify 唤醒不代表条件成立,最终永远要重新检查 predicate。

