c++的std::condition_variable如何实现线程同步? (生产者消费者模型)

std::condition_variable必须与std::mutex配合使用,所有共享数据访问及wait/notify操作均需在mutex保护下进行;需用while循环检查条件以应对虚假唤醒;notify_one适用于单消费者场景,notify_all适用于批量生产或多消费者;退出时须设置原子标志并notify_all确保线程安全终止。

std::condition_variable 必须和 std::mutex 一起用

单独声明 std::condition_variable 没有意义,它不保存状态,也不提供互斥保护。所有对共享数据的读写、以及 wait() / notify_one() 的调用,都必须在同一个 std::mutex 的保护下进行。

常见错误是只用 mutex 保护数据修改,却在 unlock 后才调用 notify_one() —— 这会导致唤醒丢失:消费者刚 wait 完、还没来得及重新加锁,生产者就 notify 了,信号被丢弃。

  • 正确做法:在持有 mutex 时修改完共享状态(如 push 到队列),**立刻**调用 notify_one()notify_all(),再 unlock
  • wait() 内部会自动 unlock mutex,并在被唤醒后重新 lock;但唤醒后仍需检查条件是否真正满足(用 while 循环,不是 if)
  • 不要依赖“通知次数”:notify_one() 只唤醒一个等待线程,但无法保证是哪个;多个消费者竞争时,要用循环重检条件

wait() 必须配合谓词(predicate)使用

wait() 的裸调用(无 lambda)极易出错,因为 spurious wakeup(虚假唤醒)在 POSIX 和 C++ 标准中都是允许的。哪怕没人 notify,线程也可能被唤醒。

所以不能写 cv.wait(lock) 然后直接消费,而必须用 while 循环检查条件是否成立:

while (queue.empty()) {
    cv.wait(lock);
}

更推荐用带谓词的重载,语义清晰且自动处理循环:

cv.wait(lock, [&] { return !queue.empty(); });
  • 谓词返回 false → 继续 wait 并自动 unlock/relock
  • 谓词返回 true → 退出 wait,lock 保持已持有状态
  • 如果用 if 而非 while,一次虚假唤醒就可能导致访问空队列,触发未定义行为

notify_one() vs notify_all() 的实际影响

在生产者-消费者模型中,多数情况用 notify_one() 就够了。它开销小、可预测性高,尤其当只有一个消费者在等时,避免唤醒所有线程抢锁。

但要注意边界场景:

  • 如果消费者逻辑里有多个 condition_variable(比如“有数据”和“缓冲区有空位”共用同一队列),或存在优先级调度,notify_one() 可能唤醒错的等待者
  • 当所有消费者都在等,而生产者一次 push 多个元素,用 notify_one() 会导致其余消费者继续沉睡,吞吐下降
  • notify_all() 不会死锁,但会引发“惊群”,所有等待线程争抢 mutex,其中仅一个能成功进入临界区,其余再次阻塞 —— 在高并发下可能拖慢整体响应

简单模型建议:单生产者 + 单消费者 → notify_one();多消费者且生产者常批量写入 → 考虑 notify_all(),但务必确保谓词检查足够轻量。

别忘了设置停止标志并通知所有等待线程

程序退出前,若还有线程在 wait(),它们会永远卡住。常见做法是引入 std::atomic done 作为全局退出信号,并在析构或 shutdown 时设为 true,再调用 notify_all()

消费者端需同时检查两个条件:

while (!done && queue.empty()) {
    cv.wait(lock);
}
if (done && queue.empty()) {
    break; // 退出循环
}
  • 只靠 notify_all() 不够:唤醒后仍要检查 done,否则可能取到旧数据后继续等下一次
  • 不能只检查 done:万一先设了 done = true,但消费者还没开始 wait,就会跳过 wait 直接 break,漏掉队列中残留数据
  • 生产者在 push 最后一批数据后,也应调用 notify_all(),确保消费者有机会处理完所有内容

没处理好退出逻辑,是调试时最难复现的 hang 问题之一 —— 它往往只在程序关闭瞬间出现,且不报错。