C++11 引入的 std::condition_variable(条件变量)是多线程编程中用于线程间同步和通信的重要机制。它允许一个线程挂起(阻塞),直到另一个线程通知它某个特定的条件已经满足。
相比于“忙等待”(Busy Waiting,即在一个循环里不停地检查变量),条件变量能节省 CPU 资源,因为线程在等待时会真正进入休眠状态。
下面我将从基本概念、核心机制、虚假唤醒、应用场景以及代码示例五个方面详细讲解。
1. 基本概念与头文件
要使用条件变量,需要包含头文件:
1 |
通常,条件变量需要配合以下两个组件一起工作:
- **
std::mutex**:用于保护共享数据。 - **
std::unique_lock<std::mutex>**:用于在等待期间自动解锁和重新加锁(注意:必须是unique_lock,不能是lock_guard,原因后面会讲)。
2. 核心操作
std::condition_variable 主要有三个核心动作:
- **
wait(lock, predicate)**:- 阻塞:当前线程释放锁,并进入休眠状态(不占用 CPU)。
- 唤醒:当收到通知或系统虚假唤醒时,线程解除阻塞。
- 重获锁:线程重新获取互斥锁。
- 检查条件:如果有
predicate(一个返回 bool 的函数或 lambda),它会检查条件是否满足。如果不满足,再次挂起。
-
notify_one():唤醒一个正在等待该条件变量的线程。 -
notify_all():唤醒所有正在等待该条件变量的线程。
3. 为什么需要 std::unique_lock?
std::lock_guard 是 RAII 风格的锁,一旦创建就锁住,直到销毁才释放。
而条件变量在调用 wait() 时,必须由库内部先解锁(让其他线程能获取锁并修改共享数据),然后在唤醒时重新加锁。std::unique_lock 提供了这种灵活的 lock() 和 unlock() 能力,而 lock_guard 不行。
4. 关键陷阱:虚假唤醒 (Spurious Wakeup)
这是一个面试常考点。线程在没有收到 notify 的情况下,也可能被操作系统唤醒。这被称为“虚假唤醒”。
错误写法(使用 if):
1 | if (queue.empty()) { |
正确写法(使用 while):
1 | while (queue.empty()) { |
C++11 简化写法(推荐):wait 函数支持第二个参数(谓词),自动帮我们处理 while 循环:
1 | // 意思是:直到 lambda 返回 true 时,才停止等待 |
5. 应用场景
- 生产者-消费者模型(Producer-Consumer):最经典场景。生产者往队列塞数据,通知消费者;消费者取数据,如果队列空则等待。
- 线程池(Thread Pool):工作线程在没有任务时挂起,主线程添加任务后通知工作线程“来活了”。
- 读写锁的实现或屏障(Barrier):等待所有线程到达某个同步点。
6. 代码示例:生产者-消费者模型
这个例子展示了一个生产者线程生产数据,一个消费者线程处理数据。
1 |
|
代码解析:
- **Consumer 的
cv.wait**:- 它首先持有了
mtx。 - 然后检查 lambda
!dataQueue.empty() || finished。 - 如果为
false(没数据且没结束):它释放锁mtx并进入休眠。 - 如果为
true:它继续持有锁往下执行。
- 它首先持有了
- **Producer 的
cv.notify_one**:- 生产者放入数据后调用
notify_one。 - 操作系统唤醒 Consumer 线程。
- Consumer 醒来后,尝试重新获取锁
mtx。 - 拿到锁后,再次执行 lambda 检查,发现队列不为空,于是往下执行取数据逻辑。
- 生产者放入数据后调用
7. 总结
-
std::condition_variable用于线程因等待某个条件而挂起。 - 必须配合
std::unique_lock<std::mutex>使用。 - 使用
wait时必须防范虚假唤醒(建议使用带谓词的wait版本)。 -
notify_one唤醒一个,**notify_all** 唤醒所有。 - 它是构建复杂并发模式(如线程池、任务队列)的基石。