文章目录
-
- 0.引言
- [1.核心组件与基本 API](#1.核心组件与基本 API)
- 2.生产者-消费者示例
- [3.为什么 wait必须与互斥锁配合使用?](#3.为什么 wait必须与互斥锁配合使用?)
- [4.notify_one 与 notify_all 的区别](#4.notify_one 与 notify_all 的区别)
- [5.谓词版本的 wait 为什么更安全?](#5.谓词版本的 wait 为什么更安全?)
- [6. 小结](#6. 小结)
0.引言
在多线程编程程序中,线程之间经常需要协同工作。常见的一种场景是:一个线程需要等待某个条件满足,再继续执行。例如:
-
消费者线程等待队列非空,然后取出数据;
-
工作线程等待某个标志位被设置,然后开始处理任务。
如果直接用最简单的轮询(busy waiting)来实现:
cpp
// 消费者线程
while (queue.empty()) {
// 什么都不做,继续循环
}
// 退出循环后,队列非空,取出数据
这种写法的问题很明显:CPU 会一直空转,浪费资源,如果系统负载高,这种空转可能导致其他线程得不到执行机会。
条件变量(std::condition_variable)正是为了解决这个问题而生的。它允许线程在条件不满足时休眠,将 CPU 让给其他线程,直到条件满足时被唤醒。这是一种高效的线程同步机制。
1.核心组件与基本 API
C++ 标准库提供了两个条件变量类:
-
std::condition_variable:只与 std::mutex 配合使用。
-
std::condition_variable_any:可与任何满足互斥体概念的对象配合,但开销更大。
在绝大多数情况下,我们使用 std::condition_variable 即可。
主要成员函数:

2.生产者-消费者示例
我们来实现一个最简单的生产者-消费者模型:
-
生产者线程向队列中放入数据,并通知消费者。
-
消费者线程等待队列非空,然后取出数据。

cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
std::queue<int> g_queue; // 共享数据队列
std::mutex g_mutex; // 保护队列的互斥锁
std::condition_variable g_cv; // 条件变量
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(g_mutex);
g_queue.push(i);
std::cout << "Produced: " << i << std::endl;
} // 离开作用域,自动解锁
g_cv.notify_one(); // 通知一个消费者线程
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(g_mutex);
// 等待,直到队列非空(条件满足)
g_cv.wait(lock, [] { return !g_queue.empty(); });
// 条件满足,取出数据
int value = g_queue.front();
g_queue.pop();
std::cout << "Consumed: " << value << std::endl;
lock.unlock(); // 可选,提前解锁
// 模拟消费耗时
std::this_thread::sleep_for(std::chrono::milliseconds(150));
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
// 为了让消费者看到结束,此处简单等待(实际项目可使用哨兵值)
std::this_thread::sleep_for(std::chrono::seconds(1));
// 注意:这里没有优雅退出,只是示例,实际应该设置停止标志
return 0;
}
关键点解释:
-
生产者使用 std::lock_guard 来锁定 g_mutex,在修改队列后立即释放锁,然后调用 notify_one()。这样唤醒时,消费者线程可以立刻获取锁。
-
消费者使用 std::unique_lock(而不是 lock_guard),因为 wait 需要在线程阻塞期间解锁,唤醒后再重新锁定,unique_lock 支持这种操作。
-
wait 的谓词版本 g_cv.wait(lock, []{ return !g_queue.empty(); }) 等价于:
cpp
while (!g_queue.empty()) {
g_cv.wait(lock);
}
它会在条件不满足时持续等待,即使被虚假唤醒(spurious wakeup)也会重新检查条件。
3.为什么 wait必须与互斥锁配合使用?
这是一个常见的问题:为什么不能单独使用条件变量?为什么 wait 必须接受一个已经锁定的 unique_lock?
考虑一个没有锁的伪代码实现:
cpp
// 错误示例:没有使用互斥锁
if (queue.empty()) {
cv.wait(); // 假设有这样的 API
}
假设线程 A 执行到 if (queue.empty()) 判断为真,正准备进入 wait,但此时操作系统切换到了生产者线程 B。B 修改了队列(使其非空),并调用了 notify_one()。由于 A 还没有进入 wait,这个通知就丢失了。接着 A 才进入 wait,它将永远等下去,因为没有人再通知它了。
这就是经典的 "丢失唤醒" 问题。解决办法是:将条件检查和进入等待这两个步骤原子化。互斥锁实现了这一点:wait 内部会原子地完成"解锁 + 阻塞"两个动作,同时保证条件检查在锁的保护下完成。具体过程如下:
1)线程在调用 wait 之前已经持有锁。
2)wait 内部首先检查条件(谓词版本会先调用谓词,如果不满足,则执行下面的步骤)。
3)原子地:释放锁 + 阻塞线程。
4)当被唤醒后,wait 重新获取锁,然后返回(或再次检查谓词)。
这样,从条件检查到进入休眠之间没有空隙,通知不会丢失。
因此,wait 必须与互斥锁配合,并且锁必须由 std::unique_lock 持有,因为 lock_guard 不支持中途释放锁。
4.notify_one 与 notify_all 的区别
notify_one():唤醒一个等待的线程(如果有多个,由调度器决定唤醒哪一个)。适用于只需一个线程处理新任务的场景,可以减少"惊群效应"。
notify_all():唤醒所有等待的线程。适用于所有线程都需要响应某个状态变化(比如程序结束标志)。
在生产者-消费者模型中,如果只有一个消费者,notify_one 就足够了。如果有多个消费者,且每次生产一个数据,通常也使用 notify_one,因为只有一个消费者能获得数据,其他被唤醒的线程会再次进入等待,造成不必要的上下文切换。
5.谓词版本的 wait 为什么更安全?
上面的示例中,我们使用了 g_cv.wait(lock, []{ return !g_queue.empty(); }); 而不是直接调用 wait(lock) 再自己检查。
理由有两个:
1)自动处理虚假唤醒:即使线程被虚假唤醒,谓词会被重新检查,如果不满足,会继续等待,避免了错误执行。
2)代码更简洁:将条件检查与等待逻辑封装在一起,减少了出错可能。
在 C++ 标准中,wait(lock, pred) 等价于:
cpp
while (!pred()) {
wait(lock);
}
因此,它已经包含了必要的循环。
6. 小结
-
条件变量解决了线程间高效等待的问题,避免了 CPU 空转。
-
基本用法:互斥锁 + 条件变量,等待条件满足。
-
wait 必须与 std::unique_lock 配合,用于原子地释放锁并阻塞。
-
使用 notify_one 或 notify_all 唤醒线程。
-
始终使用谓词版本的 wait,以正确处理虚假唤醒。
下一篇我们将深入探讨条件变量的超时机制。
更多深入内容欢迎了解:C++/Linux/ 数据库内核 | 底层开发 + AI 实战圈------12 个月系统落地,从原理到工业级实战,搭建你的核心技术壁垒
