在多线程协作中,我们经常会遇到 “一个线程需要等待另一个线程把数据准备好才能继续执行” 的场景。如果让等待线程写一个死循环不停地去检查状态变量,就会白白消耗大量的 CPU 时间。
这就好比你在家等快递:每隔几秒跑到门口看一眼非常累人,更好的办法是先在屋里安心休息,等快递员到了按响门铃通知你,再去开门取件。
C++ 在 <condition_variable> 头文件中提供的 std::condition_variable(条件变量),就是专门用来实现 “条件未满足时睡眠等待、状态更新后按铃唤醒” 的同步工具,如下图所示。

wait() 与 notify_one() 基本用法
在 C++ 中,std::condition_variable 不能单独工作,通常需要下面这 3 个部分配合使用:
- 共享状态变量:比如
bool ready = false;,用来记录数据到底准备好了没有。 - 互斥锁:比如 std::mutex,用来保护共享状态变量的读写安全。
- 条件变量:即
std::condition_variable,用来让等待线程进入睡眠,或者按门铃叫醒它。
语法:
#include <condition_variable>
std::condition_variable cv;
// 等待线程
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []() { return 条件表达式; });
// 通知线程
cv.notify_one();说明:
对于上面的等待与通知语法,我们需要重点搞清楚以下 3 个部分:
- lock(第 1 个参数):我们必须传入已加锁的 std::unique_lock 对象,不能传入
std::lock_guard。因为线程准备睡眠时,wait()需要先把锁临时解开让其他线程修改数据,醒来后再重新锁上,这只有unique_lock支持。 - 放行条件(第 2 个参数):通常传入一个返回布尔值的 Lambda 表达式,表示 “等待直到该条件成立”。返回
true说明条件已满足,直接放行往下走;返回false说明条件还没满足,此时自动释放锁并进入睡眠。小伙伴们千万不要把true和false的方向记反了。 - notify_one():由负责准备数据的线程在修改完状态变量后调用,用来唤醒 1 个正在等待该条件变量的线程。
示例 1:等待数据就绪通知
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
std::string message;
bool ready = false;
void workerTask() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []() { return ready; }); // 等待 ready 变为 true
std::cout << "工作线程收到数据:" << message << "\n";
}
int main() {
std::thread worker(workerTask);
{
std::lock_guard<std::mutex> lock(mtx);
message = "订单数据已就绪";
ready = true;
}
cv.notify_one(); // 解锁后发通知唤醒线程
worker.join();
return 0;
}运行结果如下。
工作线程收到数据:订单数据已就绪分析:
在这个例子中,工作线程在 workerTask() 里调用 cv.wait() 等待主线程把数据准备好。为了看清这两个线程是如何配合的,我们可以顺着执行先后拆成 3 步来看:
- 睡前自动解锁:工作线程先通过
unique_lock锁住mtx,再调用cv.wait()检查ready。此时ready为false,工作线程不能抱着锁去睡觉(否则主线程拿不到锁改数据),因此wait()会自动调用lock.unlock()临时放锁,再让线程进入睡眠。 - 改状态并按门铃:主线程在局部花括号里通过 std::lock_guard 拿到空闲的
mtx,填好message并将ready改为true。走出花括号自动释放锁后,主线程调用cv.notify_one()唤醒工作线程。 - 醒来重拿锁并复查:工作线程被唤醒后,
wait()会先自动调用lock.lock()重新拿回互斥锁,再次执行 Lambda 表达式确认ready已经为true,然后才正式放行并打印message。
这里顺便提一点:上面代码中的 ready 是全局变量,所以 Lambda 表达式的捕获列表写成了空的 []。如果以后我们在函数里判断局部变量或类成员变量,记得在捕获列表里写上 [&] 或 [this],否则编译器会报错提示找不到该变量。
wait() 的循环检查与虚假唤醒
为什么调用 wait() 时还要传入第 2 个条件判断参数呢?其实 cv.wait(lock, []() { return ready; }); 在底层等价于 while (!ready) { cv.wait(lock); } 循环,反复检查 ready 主要是为了解决两个问题:
防止通知先发出而错过信号:假如主线程执行得非常快,在工作线程还没跑到 wait() 之前,就已经把 ready 改成了 true 并按完了门铃。工作线程随后进入 wait() 时会先检查一次 ready,发现已经是 true 就直接往下走,不会再进入睡眠白等。
防止虚假唤醒(spurious wakeup):在底层操作系统中,睡眠中的线程偶尔会在没有任何人调用 notify 的情况下被意外唤醒。有了循环复查之后,哪怕线程被意外吵醒,只要看到 ready 还是 false 就会继续回去睡眠,不会误判条件已经成立。
notify_one() 与 notify_all()
当共享状态修改完毕后,std::condition_variable 提供了两个发送通知的成员函数:
- notify_one():如果当前有多个线程正在等待,只唤醒其中的 1 个线程。这非常适合 “来了一个新任务,只需要叫醒 1 个空闲线程去处理” 的场景。
- notify_all():一次性唤醒当前正在等待的 “所有” 线程。这非常适合 “裁判发令枪响了” 或 “全局配置加载完毕”,所有等待线程都需要继续往下执行的场景。
示例 2:广播唤醒多个线程
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
bool startFlag = false;
void runner(int id) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []() { return startFlag; });
std::cout << "选手 " << id << " 已出发\n";
}
int main() {
std::thread t1(runner, 1);
std::thread t2(runner, 2);
std::thread t3(runner, 3);
{
std::lock_guard<std::mutex> lock(mtx);
startFlag = true;
std::cout << "裁判:预备,跑!\n";
}
cv.notify_all(); // 广播唤醒所有等待线程
t1.join();
t2.join();
t3.join();
return 0;
}运行结果如下。
裁判:预备,跑!
选手 1 已出发
选手 2 已出发
选手 3 已出发分析:
cv.notify_all(); 表示向当前所有等待在 cv 上的线程发出唤醒信号。当主线程把 startFlag 设为 true 并调用 cv.notify_all() 后,t1、t2、t3 三个子线程都会被唤醒。
需要注意的是,“同时被唤醒” 并不代表三个线程会同时冲过 wait() 抢着打印。因为每个线程从 wait() 返回之前,都必须重新拿回互斥锁 mtx,所以它们实际上是排着队依次拿锁、打印并释放锁的。由于每次先抢到锁的线程由系统调度决定,小伙伴们在本地运行时看到的选手出发顺序可能和上面不同。
使用条件变量的注意事项
平时编写条件变量代码时,小伙伴们需要重点留意以下 2 个细节:
- 发通知前必须先加锁修改状态变量:
cv.notify_one()和cv.notify_all()只是一个 “提醒对方起来检查条件” 的门铃,它们本身并不保存任何布尔状态。 - 推荐先释放锁再发送通知:在通知线程中,我们推荐使用一对局部花括号
{}包裹std::lock_guard修改共享变量。走出花括号让锁先自动释放,随后再调用notify_one()或notify_all(),这样等待线程一醒来就能立刻拿到空闲的锁,不用多等一次。
对于第 1 点,如果通知线程只填了业务数据并按响门铃,却忘了把布尔状态变量 ready 改成 true,就会出问题。因为等待线程醒来复查时看到 ready 依然是 false,会误以为条件还没满足,继续回去睡眠,导致程序一直卡死。因此,下面这种漏改状态变量的写法是错误的。
void notifyBad() {
{
std::lock_guard<std::mutex> lock(mtx);
message = "订单数据已就绪";
// 漏写 ready = true;
}
cv.notify_one();
}