5、条件变量(Condition Variable):std::condition_variable的使用、生产者-消费者模型、虚假唤醒与处理
条件变量,说白了就是线程间的「信号灯」。
你想想看,如果两个线程要协作干活——一个等数据,一个生产数据。轮询检查?太浪费CPU了。直接阻塞等待?又不知道什么时候该醒。这时候就需要条件变量登场了。
我个人习惯把条件变量理解成「门卫」。线程A说:「数据好了叫我一声」,然后就去睡觉。线程B生产完数据,喊一声:「醒醒,活来了!」。嗯,就是这么个意思。
5.1 std::condition_variable 的核心用法
C++11 标准库提供了 std::condition_variable,它必须配合 std::unique_lock<std::mutex> 使用。为什么不能用 lock_guard?因为条件变量需要能够主动解锁和重新加锁,unique_lock 提供了这个灵活性。
核心API就三个:
wait(lock, predicate)—— 等条件满足,不满足就睡觉notify_one()—— 叫醒一个等待的线程notify_all()—— 叫醒所有等待的线程
重要提醒: wait() 的第二个参数是个可调用对象(通常是lambda),返回true表示条件已满足。这个参数不是可选的——我建议永远都带上它。
来看一个最基础的例子:
#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 等ready变成true
std::cout << "工作线程被唤醒了\n";
}
void notifier() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 通知等待的线程
}
int main() {
std::thread t1(worker);
std::thread t2(notifier);
t1.join();
t2.join();
return 0;
}
这段代码里,worker 线程会一直阻塞在 wait 上,直到 notifier 线程把 ready 设为 true 并调用 notify_one()。注意那个大括号——我习惯把修改共享状态和通知分开,先释放锁再通知,能减少一些不必要的竞争。
5.2 生产者-消费者模型
这是条件变量最经典的应用场景。我在项目中遇到过好几次,从日志系统到任务队列,本质上都是这个模型。
核心思路:
- 生产者往队列里放数据,放完后通知消费者
- 消费者从队列里取数据,队列为空就等着
- 队列本身需要互斥锁保护
#include <queue>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>
template<typename T>
class ThreadSafeQueue {
public:
void push(T value) {
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(std::move(value));
cv_.notify_one(); // 通知一个消费者
}
T pop() {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this]{ return !queue_.empty(); });
T value = std::move(queue_.front());
queue_.pop();
return value;
}
private:
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable cv_;
};
// 使用示例
ThreadSafeQueue<int> queue;
void producer() {
for (int i = 0; i < 10; ++i) {
queue.push(i);
std::cout << "生产: " << i << "\n";
}
}
void consumer() {
for (int i = 0; i < 10; ++i) {
int value = queue.pop();
std::cout << "消费: " << value << "\n";
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
我的经验: 生产者和消费者的数量不一定非要1:1。多个生产者、多个消费者同时工作也很常见。这时候 notify_one() 和 notify_all() 的选择就很重要了——如果多个消费者等待不同类型的数据,用 notify_all() 可能更合适。
5.3 虚假唤醒(Spurious Wakeup)
嗯,这里要注意。虚假唤醒是个坑,而且是个「系统级」的坑。
什么叫虚假唤醒?就是你的线程明明没收到 notify,却自己醒过来了。为什么会这样?说白了,操作系统在实现条件变量时,为了性能和正确性,允许线程在某些情况下「提前」返回。这不是bug,是特性。
我曾经在一个网络框架里踩过这个坑。当时写了个简单的 wait 没加条件判断,结果在高负载下偶尔会出现消费者取到空数据的情况。排查了半天,最后发现是虚假唤醒导致的。
避坑指南: 永远不要在 wait() 之后假设条件一定满足。必须用while循环或者带谓词的 wait 来重新检查条件。
正确的写法有两种:
// 方式一:while循环(C++11之前的标准做法)
std::unique_lock<std::mutex> lock(mtx);
while (!condition) {
cv.wait(lock);
}
// 方式二:带谓词的wait(推荐)
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return condition; });
方式二其实就是方式一的语法糖。底层实现里,wait 内部会帮你做while循环检查。所以我个人强烈建议:永远使用带谓词的 wait。省事,安全,不容易忘。
5.4 知识体系图
下面这张图总结了条件变量的核心知识点和它们之间的关系:
5.5 几个实用建议
最后,分享几个我在实际项目中积累的经验:
- 锁和条件变量要一一对应。 不要用一个条件变量等待多个不同的条件,容易搞混。每个逻辑条件配一个条件变量,清晰明了。
- notify 之前先解锁。 虽然
notify本身不要求锁,但如果在持有锁的情况下通知,被唤醒的线程会立即尝试获取锁,可能造成不必要的上下文切换。我习惯先解锁再通知。 - 小心死锁。 如果多个条件变量嵌套等待,很容易死锁。我一般会避免在一个等待里再去等待另一个条件变量。
- 考虑使用
std::condition_variable_any。 它跟std::condition_variable功能一样,但可以配合任何满足BasicLockable要求的锁,比如std::shared_lock。不过性能略差,一般场景用不到。
一个小技巧: 调试条件变量相关的问题时,可以在 wait 前后加日志,记录线程ID和时间戳。我曾经靠这个快速定位了一个「消费者永远等不到数据」的bug——原来是生产者忘记调用 notify 了。
条件变量用好了,线程间的协作会非常优雅。用不好,就是各种诡异的挂起和超时。记住核心原则:永远检查条件,永远不要假设唤醒就是正确的信号。做到这一点,大部分坑你都能绕过去。
公众号:蓝海资料掘金营,微信deep3321