4. 条件变量与同步:std::condition_variable、生产者-消费者模型、wait与notify机制、虚假唤醒处理
条件变量,说白了就是线程之间的“传话筒”。
我刚开始学多线程时,总觉得互斥锁就够了。后来发现,很多时候线程需要“等某个条件成立”才能继续干活。比如消费者线程等着队列里有数据,生产者线程等着队列有空位。这时候如果只用锁,就得不停地轮询检查——CPU 白烧了,效率也低。
条件变量就是来解决这个问题的。它让线程可以“睡一会儿”,等条件满足了再被叫醒。
4.1 std::condition_variable 是什么?
std::condition_variable 是 C++11 引入的同步原语。它必须和 std::mutex 配合使用,不能单独工作。
它的核心操作就两个:等待和通知。
- wait:线程释放锁,进入阻塞状态,直到被唤醒。
- notify_one / notify_all:唤醒一个或所有等待的线程。
我习惯把条件变量想象成一个“休息室”。线程进去休息前,先把锁交出来。等有人喊“条件满足了”,线程再醒来,重新拿回锁,继续干活。
4.2 生产者-消费者模型:最经典的例子
生产者-消费者模型,是多线程编程的“Hello World”。
一个线程生产数据,放到共享队列里。另一个线程从队列里取数据消费。队列满了,生产者就得等。队列空了,消费者就得等。
下面是我写的一个简化版实现:
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>
std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;
const int MAX_SIZE = 10;
void producer() {
int value = 0;
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 队列满了,等待消费者消费
cv.wait(lock, []{ return q.size() < MAX_SIZE; });
q.push(value++);
std::cout << "生产: " << value - 1 << "\n";
lock.unlock();
cv.notify_one(); // 通知消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !q.empty(); });
int val = q.front();
q.pop();
std::cout << "消费: " << val << "\n";
lock.unlock();
cv.notify_one(); // 通知生产者
std::this_thread::sleep_for(std::chrono::milliseconds(150));
}
}
你看,wait 的第二个参数是个 lambda 表达式,用来检查条件是否真的满足。这个设计很关键——它直接帮我们处理了虚假唤醒的问题。
4.3 wait 与 notify 机制:到底怎么工作的?
很多人搞不清楚 wait 内部到底干了啥。我拆开来说:
- 线程调用
cv.wait(lock, predicate)。 - 检查
predicate()是否为 true。如果是,直接返回,不阻塞。 - 如果为 false,释放锁,线程进入阻塞状态。
- 被唤醒后,重新获取锁,再次检查
predicate()。 - 如果条件满足,返回。否则继续阻塞。
说白了,wait 就是一个“检查-阻塞-再检查”的循环。只不过这个循环由库帮你实现了,你不用自己写 while 循环。
而 notify_one 和 notify_all 就是用来唤醒线程的。注意,notify 本身不释放锁。被唤醒的线程要等 notify 的线程释放锁后,才能拿到锁继续执行。
关键点:notify_one 只是“叫醒”一个线程,但那个线程不一定能立刻运行。它得等当前线程释放锁。所以 notify 之后尽快 unlock,可以减少线程切换的延迟。
4.4 虚假唤醒:你永远躲不过的坑
虚假唤醒,听起来很玄乎。其实就是线程被唤醒了,但条件并没有真正满足。
为什么会这样?
操作系统在实现条件变量时,出于性能考虑,可能会一次唤醒多个线程。或者信号在传递过程中出现了“惊群效应”。总之,你不能假设被唤醒就一定条件满足。
我曾经在一个项目中踩过这个坑。当时写了一个消费者线程,直接用 cv.wait(lock),没有传 predicate。结果偶尔会出现队列为空时,线程被唤醒,然后去 pop 空队列——程序直接崩溃。
避坑指南:永远使用带 predicate 的 wait 重载,或者在 wait 之后手动加 while 循环检查条件。
正确的写法就两种:
// 方式一:带 predicate 的 wait(推荐)
cv.wait(lock, []{ return !q.empty(); });
// 方式二:手动 while 循环
while (q.empty()) {
cv.wait(lock);
}
我个人习惯用第一种,代码更简洁,意图也更清晰。
4.5 知识体系总览
下面这张图,把条件变量的核心知识点串起来了:
4.6 实战中的几个小建议
最后,分享几个我在项目中积累的经验:
- 尽量用
notify_one而不是notify_all。除非你确定所有等待线程都能干活,否则唤醒一堆线程只会增加竞争。我见过有人图省事,一律用 notify_all,结果性能直接掉了一半。 - 注意锁的粒度。在
wait之前,锁已经释放了。但wait返回后,锁又被重新获取。所以临界区要尽量小,不要在持有锁的时候做耗时操作。 - 小心死锁。如果两个线程互相等待对方的条件变量,就会死锁。设计时一定要理清依赖关系。
小技巧:调试条件变量相关的问题时,可以在 wait 前后加日志,打印线程 ID 和当前条件状态。这样能快速定位是没被唤醒,还是条件判断出了问题。
嗯,条件变量这块内容,说难不难,说简单也不简单。关键是要理解它的设计意图——让线程在条件不满足时“睡大觉”,而不是“干着急”。
你想想看,如果每次都要轮询检查,CPU 得多累啊。条件变量就是那个“闹钟”,条件满足了再叫你起来干活。