必须用wait()谓词重载版本防虚假唤醒;notify_all()适用于非独占资源如缓冲区空满;锁须包裹条件检查与操作全过程;条件变量生命周期须长于等待线程。

用 std::mutex + std::condition_variable 实现生产者消费者模型是 C++ 多线程中最典型也最容易出错的实践之一。核心不在于“能不能跑”,而在于“唤醒是否可靠”“边界是否守得住”“虚假唤醒是否被处理”。下面直奔关键点。
为什么必须用 wait() 的谓词重载版本?
直接调用 cv.wait(lock) 是危险的——它不检查条件是否真正满足,只负责挂起和被唤醒。一旦发生虚假唤醒(spurious wakeup),线程会跳过条件判断直接往下执行,导致读空队列或写满队列。
正确做法是始终使用带谓词的重载:cv.wait(lock, [&]{ return !queue.empty(); });
- 该重载内部自动循环检查谓词,天然防虚假唤醒
- 谓词必须捕获所有依赖变量(如
queue),且不能抛异常 - 不要写成
while(queue.empty()) cv.wait(lock);—— 手动 while 容易漏锁或逻辑错位
notify_one() 还是 notify_all()?
多数场景下用 notify_one() 更高效,但前提是:你明确知道每次只该唤醒一个等待者。
立即学习“C++免费学习笔记(深入)”;
生产者消费者中,常见错误是:
- 生产者调用
notify_one(),但多个消费者在等——只有一个被唤醒,其余继续挂起,没问题 - 消费者调用
notify_one(),但多个生产者在等——同理,也 OK - 但若队列容量有限(如环形缓冲区),且生产者需等待“有空位”,此时多个生产者可能同时被阻塞;如果只 notify_one,其他生产者永远收不到信号——这时必须用
notify_all()
更稳妥的做法:对“非独占资源”(如缓冲区空/满)统一用 notify_all();对“一对一唤醒”(如任务就绪)可用 notify_one()。
如何避免死锁和竞争?
最常踩的坑是锁粒度与条件检查脱节。例如:
std::queue<int> queue;
std::mutex mtx;
std::condition_variable cv;
// ❌ 错误:先检查再加锁,中间存在竞态
if (queue.empty()) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [&]{ return !queue.empty(); }); // 可能永远等下去
}
// ✅ 正确:锁必须包裹整个条件检查 + wait + 操作
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [&]{ return !queue.empty(); });
int val = queue.front();
queue.pop();
- 所有对共享数据(
queue)的访问,包括empty()、front()、pop(),都必须在std::unique_lock保护下 -
wait()内部会自动释放锁,并在唤醒后重新获取——这个机制不能绕过 - 不要用
std::lock_guard替代std::unique_lock,因为wait()要求可转移、可释放的锁
要不要用 std::deque 或无锁结构替代 std::queue?
std::queue 默认基于 std::deque,本身不是线程安全的,所以仍需外部同步。换成 std::deque 并不会减少锁需求——只是让你多一层手动管理 push_back()/pop_front() 的麻烦。
真要优化性能,考虑以下路径:
- 单生产者单消费者(SPSC):可用
boost::lockfree::queue或自实现环形缓冲区,避开锁 - 多生产者多消费者(MPMC):标准库无原生支持,
moodycamel::ConcurrentQueue是较成熟选择 - 但除非压测确认锁是瓶颈,否则别过早替换——
std::mutex+std::condition_variable在现代 glibc/libstdc++ 上已足够高效
真正容易被忽略的是:条件变量的生命周期必须长于所有等待它的线程。如果主线程析构了 cv,而子线程还在 wait(),行为未定义——务必确保同步对象的销毁顺序。









