Wiki
condition_variable
An English translation is not available yet. The latest Simplified Chinese source is shown without triggering paid translation.
std::condition_variable 用于让一个线程等待某个共享条件发生变化,而不是不停循环检查。
典型场景包括:
- 生产者把数据放入队列,消费者没有数据时等待;
- 一个线程等待初始化完成;
- 一个工作线程等待新任务到来;
- 多个线程等待某个状态变为 ready。
条件变量最重要的不是记住 wait() 和 notify_one(),而是理解它必须围绕下面三件东西一起使用:
共享状态 + mutex + condition_variable
1.为什么不能一直轮询
最直接的等待方式可能是:
while (!ready)
{
}
这种写法有两个问题:
- 如果
ready是普通变量,多线程读写本身就可能产生数据竞争; - 即使用
atomic,这种忙等也会持续占用 CPU。
例如:
while (!ready.load())
{
}
在等待时间较长时通常不是理想方案。
条件变量允许线程进入阻塞等待,等到其他线程通知后再继续。
2.基本组成
通常会同时存在:
std::mutex mutex;
std::condition_variable cv;
bool ready = false;
这里:
ready:真正表示业务条件的共享状态;mutex:保护ready;cv:负责睡眠和唤醒等待线程。
注意:
condition_variable本身不是状态。
它只是通知机制。真正决定线程能否继续的是谓词所检查的共享状态。
3.最简单的等待与通知
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mutex;
std::condition_variable cv;
bool ready = false;
void worker()
{
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [] {
return ready;
});
std::cout << "worker start\n";
}
int main()
{
std::thread t(worker);
{
std::lock_guard<std::mutex> lock(mutex);
ready = true;
}
cv.notify_one();
t.join();
}
执行逻辑:
worker 获得 mutex
↓
检查 ready == false
↓
wait 暂时释放 mutex,并进入等待
↓
main 获得 mutex
↓
修改 ready = true
↓
main 释放 mutex
↓
notify_one
↓
worker 被唤醒并重新获得 mutex
↓
再次检查 ready
↓
条件成立,wait 返回
4.为什么 wait() 要配合 std::unique_lock
常见写法是:
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, predicate);
而不是:
std::lock_guard<std::mutex>
原因在于 wait() 必须完成一组特殊操作:
- 当前线程已经持有 mutex;
wait()在睡眠前临时释放 mutex;- 线程被唤醒后重新获取 mutex;
- 获得 mutex 后再返回给调用者。
unique_lock 支持这种“暂时释放、重新获得”的所有权管理,而 lock_guard 不提供手动解锁和重新加锁能力。
5.wait(lock) 与 wait(lock, predicate)
条件变量有两种常见等待形式。
5.1.不带谓词
cv.wait(lock);
线程被唤醒后直接返回。
如果使用这种形式,就必须自己写循环检查条件:
while (!ready)
{
cv.wait(lock);
}
5.2.带谓词
推荐写:
cv.wait(lock, [] {
return ready;
});
它可以理解为标准库帮你写了:
while (!ready)
{
cv.wait(lock);
}
因此大多数普通场景优先使用带谓词版本。
6.虚假唤醒(spurious wakeup)
等待线程有可能在没有对应业务事件发生时从 wait() 醒来,这叫虚假唤醒。
因此下面这种逻辑不可靠:
cv.wait(lock);
use_shared_data();
正确思路是:
每次醒来都重新检查真正的共享条件。
也就是:
cv.wait(lock, [] {
return ready;
});
这也是为什么“谓词”不是可有可无的装饰。
7.条件变量没有记忆:不要把 notify 当状态
另一个常见误解是:
调用了
notify_one(),以后某个线程再wait()时应该能收到这次通知。
不是。
条件变量不会像消息队列一样永久保存通知。
例如:
线程 A notify_one()
↓
当时没有线程正在等待
↓
这次通知结束
↓
线程 B 之后才开始 wait()
线程 B 不会自动“补收到”之前的通知。
因此必须用共享状态记录事实:
ready = true;
cv.notify_one();
即使通知时没有线程正在睡眠,未来线程进入:
cv.wait(lock, [] { return ready; });
也会先检查到 ready == true,从而不需要睡眠。
这就是:
共享状态保存事实
condition_variable 负责提高等待效率
8.notify_one() 和 notify_all()
8.1.notify_one()
cv.notify_one();
唤醒一个正在等待的线程。
适合:
- 新增一个任务,只需要一个 worker 处理;
- 只需要一个消费者继续执行。
8.2.notify_all()
cv.notify_all();
唤醒所有正在等待的线程。
适合:
- 全局状态改变,所有线程都需要重新检查条件;
- 程序停止,需要让所有等待线程退出。
被唤醒不等于所有线程能同时进入临界区。它们仍然需要竞争同一把 mutex。
9.修改状态后什么时候 notify
一种常见推荐写法是:
{
std::lock_guard<std::mutex> lock(mutex);
ready = true;
}
cv.notify_one();
即:
- 持锁修改共享状态;
- 释放锁;
- 再通知。
这样等待线程醒来后更有机会直接获得 mutex,而不是醒来后马上又因为通知线程仍持锁而阻塞。
不过关键正确性原则不是“notify 必须永远写在锁外”,而是:
对共享谓词状态的访问必须正确同步,等待方必须在锁保护下检查谓词。
10.生产者消费者
这是条件变量最经典的应用。
#include <condition_variable>
#include <iostream>
#include <mutex>
#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<std::mutex> lock(mutex);
queue.push(value);
}
cv.notify_one();
}
{
std::lock_guard<std::mutex> lock(mutex);
finished = true;
}
cv.notify_all();
}
void consumer()
{
while (true)
{
int value = 0;
{
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [] {
return !queue.empty() || finished;
});
if (queue.empty() && finished)
{
break;
}
value = queue.front();
queue.pop();
}
// 真正处理数据放在锁外
std::cout << "consume " << value << '\n';
}
}
int main()
{
std::thread p(producer);
std::thread c(consumer);
p.join();
c.join();
}
这里谓词不是单纯:
!queue.empty()
而是:
!queue.empty() || finished
否则生产者彻底结束后,如果队列已经为空,消费者可能永远继续等待,再也没人通知它有数据。
11.为什么取出任务后要尽快释放锁
消费者应该在锁内只做:
检查队列
取出任务
修改共享状态
真正耗时的任务执行应尽量放到锁外。
如果消费者拿着 queue 的 mutex 执行一个耗时 2 秒的任务,那么生产者和其他消费者这 2 秒内都可能无法访问队列。
12.超时等待:wait_for()
cv.wait_for(lock, duration)
可以等待一段时间。
推荐同样使用谓词版本:
bool ok = cv.wait_for(
lock,
std::chrono::seconds(1),
[] {
return ready;
});
返回:
true:谓词最终成立;false:超时且谓词仍未成立。
示例:
if (!cv.wait_for(lock, std::chrono::seconds(1), [] {
return ready;
}))
{
std::cout << "timeout\n";
}
13.绝对时间等待:wait_until()
cv.wait_until(lock, time_point, predicate)
适合已经有一个明确截止时间点的场景。
例如:
auto deadline = std::chrono::steady_clock::now()
+ std::chrono::seconds(2);
cv.wait_until(lock, deadline, [] {
return ready;
});
涉及超时时,通常优先使用 steady_clock,因为它不会受到系统墙上时钟被手动修改的影响。
14.一个条件变量可以等待多个条件吗?
可以。
真正决定等待逻辑的是谓词,例如:
cv.wait(lock, [] {
return !queue.empty() || stopped || error;
});
醒来后再根据具体状态分支处理:
if (error)
{
...
}
else if (stopped && queue.empty())
{
...
}
else
{
...
}
条件变量只是提示“相关状态可能变化了”,最终必须重新检查状态本身。
15.std::condition_variable_any
普通:
std::condition_variable
主要配合:
std::unique_lock<std::mutex>
标准库还提供:
std::condition_variable_any
它可以配合更广泛的 BasicLockable 锁类型,灵活性更高,但一般也可能带来更多开销。
普通 std::mutex 场景优先使用 std::condition_variable。
C++20 中,condition_variable_any 还提供了能与 std::stop_token 配合的等待重载,这在 std::jthread 协作式停止里很有用。
16.条件变量与 atomic 怎么选
如果只是:
等待一个原子值改变
C++20 可以考虑:
atomic.wait()
atomic.notify_one()
atomic.notify_all()
如果条件涉及:
- 队列是否为空;
- 多个字段组合;
- 复杂业务状态;
通常:
mutex + condition_variable + predicate
更自然。
17.常见错误
17.1.错误 1:把通知本身当成状态
cv.notify_one();
不会永久保存一条“通知消息”。必须有共享状态记录事实。
17.2.错误 2:不用谓词处理虚假唤醒
cv.wait(lock);
use_data();
应该重新检查条件。
17.3.错误 3:修改谓词状态时没有使用同一套同步规则
等待方在 mutex 下读取 ready,修改方却在没有 mutex 的情况下写普通 bool ready,仍然可能是数据竞争。
17.4.错误 4:持锁执行耗时任务
队列取任务后应尽量解锁,再处理任务。
17.5.错误 5:程序停止时只修改 finished,却忘记通知等待线程
等待线程可能一直睡眠。停止路径通常要配合 notify_all()。
17.6.错误 6:只检查 queue.empty(),没有设计“不会再产生数据”的结束状态
生产者消费者模型必须考虑线程如何正常退出。
18.小结
condition_variable必须围绕“共享状态 + mutex + 谓词”理解。wait()在睡眠时会临时释放 mutex,醒来后重新获得,因此通常配合unique_lock。- 条件变量允许虚假唤醒,所以应始终重新检查真正的条件。
notify_one()唤醒一个等待者,notify_all()唤醒全部等待者。- 条件变量不会永久保存通知,真正的状态必须由受同步保护的数据记录。
wait_for()/wait_until()可以实现超时等待。- 生产者消费者中,应在锁内快速取出任务,在锁外执行耗时工作。