std::condition_variable¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++11 | 3/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-11-03 |
编译器支持:GCC 4.4+ · Clang 3.0+ · MSVC 16.0+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 用 condition_variable::wait_for 配合谓词实现可中断的周期任务 | ev-spdlog-condvar-periodic-worker-001 |
| 用 condition_variable::wait 等待队列腾出空间 | ev-spdlog-condvar-queue-not-full-002 |
| 线程池工作线程在无任务时用 condition_variable::wait 休眠 | ev-stdexec-condvar-worker-sleep-003 |
本站证据页:ev-spdlog-condvar-periodic-worker-001 · ev-spdlog-condvar-queue-not-full-002 · ev-stdexec-condvar-worker-sleep-003
解决什么问题¶
线程要等待另一个线程把数据准备好,最直接的办法是循环检查一个标志。忙等会占满 CPU,还会因为编译器优化或缓存可见性写出永远退不出的循环。互斥量只能保护数据,不能让线程“睡着等条件成立”。
std::condition_variable 把“释放锁并挂起”和“被通知后重新加锁”合成一个原子式的等待:wait 在进入等待前解锁互斥量,被唤醒后重新加锁再返回,调用方因此可以在持锁状态下检查条件。标准提案为 N2497,头文件是 <condition_variable>。
基础用法¶
wait 必须配合 std::unique_lock<std::mutex>;等待期间锁被释放,其他线程可以修改共享状态并调用 notify_one 或 notify_all。始终使用带谓词的重载,它能处理虚假唤醒并减少手写循环。
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
int main() {
std::mutex mutex;
std::condition_variable cv;
std::queue<int> queue;
bool done = false;
std::thread producer([&] {
for (int value = 1; value <= 3; ++value) {
{
std::lock_guard<std::mutex> lock(mutex);
queue.push(value);
}
cv.notify_one();
}
{
std::lock_guard<std::mutex> lock(mutex);
done = true;
}
cv.notify_one();
});
std::thread consumer([&] {
int count = 0;
int total = 0;
while (true) {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [&] { return !queue.empty() || done; });
while (!queue.empty()) {
total += queue.front();
queue.pop();
++count;
}
if (done) {
break;
}
}
std::cout << count << ' ' << total << '\n';
});
producer.join();
consumer.join();
}
输出 3 6。生产者在锁内把 1、2、3 放入队列并通知;消费者在谓词成立前持续等待,醒来后一次排空队列,看到 done 后退出。完整版本见 examples/cpp11/condition_variable.cpp。
谓词由调用方提供,wait(lock, pred) 等价于“循环检查 pred(),为假就等待”。条件变量本身不保存状态,状态必须放在共享变量里,并由同一把互斥量保护。
进阶用法¶
通知可以在持锁或不持锁时发出。持锁通知会让被唤醒线程立刻又阻塞在锁上,通常先解锁再 notify_one;但不持锁通知要求共享状态已经被正确发布。两种写法都正确,关键是通知前状态已经更新。
notify_one 只唤醒一个等待者,适合生产者/消费者队列;notify_all 唤醒全部等待者,适合状态变化会影响所有线程的场景,例如关闭标志。被唤醒的线程仍需重新竞争锁并重新检查谓词。
wait_for 和 wait_until 提供超时版本,返回 std::cv_status 或谓词结果。周期任务常用 wait_for(lock, interval, pred):超时说明该执行下一轮,谓词成立说明收到停止信号。
条件变量不能复制,也不能在等待期间销毁。等待线程持有的 unique_lock 会在 wait 内部解锁再重新加锁,因此等待期间不能依赖锁的保护访问共享状态。
C++20 的 std::atomic::wait 与 std::counting_semaphore 覆盖了一部分场景:只需要等待一个整数或计数的线程,可以不再单独配互斥量与条件变量。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| spdlog | include/spdlog/details/periodic_worker.h#L31-L39 |
用 wait_for 加谓词实现可中断的周期任务 |
超时执行任务与停止信号合并到一次等待,析构时能立即退出 |
| spdlog | include/spdlog/details/mpmc_blocking_q.h#L31-L38 |
用 wait 等待有界队列腾出空间 |
谓词检查队列未满,避免虚假唤醒后越界写入 |
| stdexec | include/exec/static_thread_pool.hpp#L1145-L1159 |
工作线程无任务时在条件变量上休眠,避免空转 | 原子状态转换与条件变量等待配合,控制线程的睡眠与唤醒 |
三条证据均为 manual 级:作者复核过出处与行号,尚未引入第二名审核者。三处都在生产路径,覆盖周期任务、有界队列与线程池休眠。
陷阱与边界¶
等待必须带谓词。 条件变量可能虚假唤醒,也可能在通知与等待之间错过通知;wait(lock, pred) 会重新检查条件,裸 wait(lock) 需要自己写循环。
丢失唤醒。 如果通知发生在等待线程检查条件之后、真正进入等待之前,通知会丢失;把状态更新与谓词检查放在同一把锁下可以避免这个窗口。
wait 需要 unique_lock。 condition_variable 要求等待期间能解锁并重新加锁,lock_guard 做不到;condition_variable_any 接受任意可锁类型,但更重。
通知前必须更新状态。 只调用 notify_one 而共享变量没变,被唤醒线程会重新检查谓词并继续等待;通知本身不是消息。
析构时不能有等待者。 销毁仍有线程在 wait 的条件变量是未定义行为;关闭流程要先设置停止标志、notify_all,再 join 所有等待线程。
notify_one 可能唤醒错误线程。 多个等待者对应不同条件时,单个通知可能被无关线程消费;按条件分组使用不同条件变量,或改用 notify_all。
与互斥量的锁顺序要一致。 条件变量不解决死锁;等待线程和通知线程都必须以同一顺序获取相关锁。
库联动¶
std::mutex/std::unique_lock:条件变量的标准搭档;wait在内部解锁并在返回前重新加锁。std::queue/ 环形缓冲:生产者/消费者模型的共享状态,通常与有界队列和两个条件变量配合。std::thread:等待线程与通知线程的创建和汇合;关闭顺序通常是“置停止位 → notify_all → join”。std::atomic:简单标志可以用原子量加wait/notify替代条件变量,减少一把锁。std::counting_semaphore:C++20 的信号量适合纯计数场景,不必手写谓词。
横向对比¶
| 方案 | 需要互斥量 | 虚假唤醒处理 | 超时支持 | 主要代价 |
|---|---|---|---|---|
std::condition_variable |
是 | 谓词重载自动处理 | 是 | 必须正确配对状态、锁与通知 |
| 忙等 | 否 | 不适用 | 是 | 占满 CPU,容易写出不可见循环 |
std::atomic::wait |
否 | 平台负责 | 否 | C++20 起可用,只适合单值条件 |
std::counting_semaphore |
否 | 不适用 | 是 | C++20 起可用,语义偏计数而非谓词 |
跨标准演进:C++11 通过 N2497 引入 condition_variable 与 condition_variable_any;C++20 增加 atomic::wait/notify 与信号量,为简单等待场景提供更轻的替代;条件变量的核心“锁 + 谓词 + 通知”模型没有变化。