一、先问一个真实工程问题
你有一个任务队列:
- 生产者线程往队列里放任务
- 消费者线程从队列里取任务
当队列空时,消费者怎么办?
错误做法 1:忙等
cpp
while (queue.empty()) {
// 什么也不做
}
问题:
- CPU 100%
- 浪费资源
- 多线程系统直接爆炸
这叫:Busy Wait(忙等)
二、正确思路:线程应该"睡眠"
当队列为空:
线程应该休眠,直到有数据。
这就是:
cpp
std::condition_variable
三、Java 类比:wait / notify
Java 写法:
java
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait();
}
}
生产者:
java
synchronized (lock) {
queue.add(task);
lock.notify();
}
C++ 的 condition_variable 就是这个机制。
四、condition_variable 的三件套
必须一起使用:
cpp
std::mutex
std::unique_lock
std::condition_variable
注意:
必须是 unique_lock,不能是 lock_guard
因为等待时会:
1️⃣ 自动释放锁
2️⃣ 线程睡眠
3️⃣ 被唤醒后重新加锁
五、最小示例(错误写法)
cpp
std::unique_lock<std::mutex> lock(mtx);
while (queue.empty()) {
cv.wait(lock);
}
你可能觉得没问题。
但这里隐藏两个重大风险:
⚠ 风险 1:假唤醒(Spurious Wakeup)
线程可能:
在没有 notify 的情况下醒来。
如果你写成:
cpp
if (queue.empty()) {
cv.wait(lock);
}
那就危险了。
正确写法必须是:
cpp
while (queue.empty()) {
cv.wait(lock);
}
或者:
cpp
cv.wait(lock, []{ return !queue.empty(); });
⚠ 风险 2:丢通知
如果:
生产者先 notify
消费者还没进入 wait
那通知就丢了。
正确姿势:
状态必须由 mutex 保护
等待必须基于状态判断
六、正确模型:BlockingQueue 最小闭环
我们写一个工程级可用的阻塞队列。
1️⃣ 类定义
cpp
#include <queue>
#include <mutex>
#include <condition_variable>
template<typename T>
class BlockingQueue {
public:
void push(const T& value) {
std::unique_lock<std::mutex> lock(mtx_);
queue_.push(value);
lock.unlock(); // 解锁后通知(推荐顺序)
cv_.notify_one();
}
T pop() {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] {
return !queue_.empty();
});
T value = queue_.front();
queue_.pop();
return value;
}
private:
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable cv_;
};
2️⃣ 使用示例
cpp
#include <thread>
#include <iostream>
BlockingQueue<int> queue;
void producer() {
for (int i = 0; i < 5; ++i) {
queue.push(i);
std::cout << "Produced: " << i << std::endl;
}
}
void consumer() {
for (int i = 0; i < 5; ++i) {
int value = queue.pop();
std::cout << "Consumed: " << value << std::endl;
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
七、notify_one vs notify_all
| 方法 | 作用 |
|---|---|
| notify_one | 唤醒一个线程 |
| notify_all | 唤醒所有线程 |
推荐:
- 单消费者 → notify_one
- 多消费者 → notify_all(视场景)
八、工程级关键原则(必须记住)
1️⃣ 等待必须带 predicate
永远使用:
cpp
cv.wait(lock, condition);
不要自己写 while + wait。
2️⃣ 状态修改必须在锁内
错误:
cpp
queue.push(value);
cv.notify_one();
正确:
cpp
std::unique_lock lock(mtx);
queue.push(value);
lock.unlock();
cv.notify_one();
3️⃣ 锁保护状态,cv 只负责"通知"
condition_variable 不保护数据。
它只是:
睡眠机制 + 唤醒机制
九、系统取向思维升级
现在你的并发体系已经三层:
第一层:线程模型
共享什么?
第二层:资源保护
mutex + RAII
第三层:线程协作
condition_variable
十、和 Java 再对比
| Java | C++ |
|---|---|
| synchronized | mutex + RAII |
| wait | cv.wait |
| notify | cv.notify_one |
| notifyAll | cv.notify_all |
区别:
C++ 更底层,更灵活,也更容易写错。
十一、本篇总结口诀
有共享,用 mutex
要等待,用 cv
等待带条件
状态在锁内
十二、下一篇预告
第五篇我们进入:
std::atomic ------ 原子操作与状态一致性
- atomic 和 mutex 到底怎么分工?
- 为什么 atomic 不等于无锁系统?
- 内存可见性问题
- 停止标志(stop flag)工程示例