直接用condition_variable易卡死,因其无状态,需手动维护缓冲区状态并与mutex严格配对;常见错误包括wait前未加锁、漏判条件、误用notify_one()。

为什么直接用 condition_variable 容易卡死?
因为 condition_variable 本身不带状态,它只负责“通知”和“等待”,你得自己维护缓冲区的空/满状态,而且必须和 mutex 严格配对。常见错误是:在 wait() 前没加锁、漏判条件、或用 notify_one() 但实际需要唤醒多个线程。
实操建议:
- 所有对共享队列的读写操作,必须包裹在
std::unique_lock<:mutex></:mutex>中 -
wait()的第二个参数必须是 lambda,且内部要检查真实条件(比如!queue.empty()),不能只靠通知触发 - 生产者用
notify_one()或notify_all()都行,但消费者一般用notify_one()更轻量;若存在多个消费者竞争,notify_one()是安全的 - 别在
wait()外面手动lock()/unlock()——unique_lock的 RAII 机制会自动处理
一个能跑通的最小完整示例长什么样?
下面这段代码去掉注释就能编译运行(C++11 起支持),重点看三处:构造函数初始化、push() 和 pop() 中的 wait 逻辑、以及 stop() 如何避免死等。
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
struct BlockingQueue {
std::queue<int> q;
std::mutex mtx;
std::condition_variable not_empty, not_full;
bool stopped = false;
void push(int x) {
std::unique_lock<std::mutex> lk(mtx);
not_full.wait(lk, [this]{ return q.size() < 10 || stopped; });
if (stopped) return;
q.push(x);
not_empty.notify_one();
}
int pop() {
std::unique_lock<std::mutex> lk(mtx);
not_empty.wait(lk, [this]{ return !q.empty() || stopped; });
if (q.empty()) return -1; // stopped
int x = q.front(); q.pop();
not_full.notify_one();
return x;
}
void stop() {
std::unique_lock<std::mutex> lk(mtx);
stopped = true;
not_empty.notify_all();
not_full.notify_all();
}
};
注意:not_full 条件里用了 q.size() ,这是模拟有界队列;如果做无界队列,可以把 <code>not_full 相关逻辑全删掉,只留 not_empty。
立即学习“C++免费学习笔记(深入)”;
多生产者多消费者下,notify_one() 和 notify_all() 怎么选?
选 notify_one() 就够了。除非你明确知道某个通知要同时唤醒所有等待线程(比如广播停止信号),否则 notify_all() 会引发“惊群效应”——多个线程被唤醒后抢锁、再发现条件不满足、又回去等,白白消耗 CPU。
使用场景差异:
- 生产者调用
push()后,只唤醒一个阻塞的消费者即可 —— 用notify_one() - 消费者调用
pop()后,只唤醒一个阻塞的生产者即可 —— 同样用notify_one() -
stop()必须用notify_all(),否则可能有线程永远卡在wait()里
性能影响:在高并发下,notify_all() 可能使线程调度开销翻几倍,尤其当等待线程数 > 10 时,差异明显。
为什么 std::queue 不是线程安全的,但没人用 std::deque 替代?
因为线程安全与否跟容器类型无关,只跟访问方式有关。std::queue 默认用 std::deque 作底层容器,它本身不是“更安全”,只是默认实现恰好支持高效头尾操作。真正关键的是:所有访问都必须受同一把 mutex 保护。
容易踩的坑:
- 误以为换成
std::vector作底层容器就能省锁 —— 错,push_back()和pop_front()仍需同步 - 在
wait()的 lambda 里调用非原子操作(比如q.size() > 0 && q.front() > 5)—— 两次访问之间可能被其他线程修改,应拆成两步或加锁判断 - 忘记
stopped标志也要在mutex下读写,否则可能读到撕裂值
复杂点在于:条件变量 + 互斥锁 + 共享状态这三者的生命周期和作用域必须完全对齐,差一点就出现竞态或假唤醒。调试时最常看到的错误是 std::system_error: Invalid argument,基本都是 mutex 没 lock 就传给 wait() 导致的。










