使用 std::mutex 和 std::condition_variable 安全实现生产者消费者队列:mutex 保护共享访问,condition_variable 解决空/满等待唤醒;必须用 unique_lock 持锁调 wait(),谓词需 while 循环防虚假唤醒;优先用 std::queue(底层 deque)而非 vector 避免 reallocation 风险;固定容量队列须精确区分空/满状态,推荐单 condition_variable 配 size 谓词。

用 std::mutex + std::condition_variable 控制共享队列的访问
生产者消费者本质是线程间对有限缓冲区的协同操作,核心不是“怎么启线程”,而是“怎么安全地 push/pop + 等待/唤醒”。std::mutex 防止并发修改队列,std::condition_variable 解决空时消费者别瞎等、满时生产者别硬塞的问题。
常见错误现象:std::queue::pop() 在空队列上调用导致未定义行为;多个线程同时 push 后发现元素丢失;消费者线程 CPU 占用 100%(忙等)。
- 必须在
std::unique_lock<:mutex></:mutex>持有状态下调用wait(),否则抛std::system_error -
wait()的谓词 lambda 必须是「真实条件检查」,不能只写queue.empty()就 return —— 要用while循环重检,防虚假唤醒 - 生产者
notify_one()足够,除非你明确需要唤醒所有等待消费者(比如广播场景)
为什么不用 std::deque 或 std::vector 替代 std::queue
std::queue 是适配器,默认底层用 std::deque,它支持两端高效插入删除,且不涉及内存重分配导致的迭代器失效问题——这对加锁范围小、避免长时间持锁很关键。
用 std::vector 自己实现队列容易踩坑:每次 push_back() 可能触发 reallocation,若此时另一个线程正在 front() 或遍历,就会访问野指针;而 std::deque 的分段内存结构让单次 push/pop 基本不牵连其他元素。
立即学习“C++免费学习笔记(深入)”;
-
std::queue接口干净,天然屏蔽了不需要的操作(如随机访问),减少误用可能 - 如果真要换底层容器,只能用
std::deque,std::list也行但缓存局部性差,性能通常不如std::deque - 别在锁内做耗时操作(比如深拷贝大对象),先取出来再处理,缩短临界区
如何避免死锁和资源耗尽(特别是固定大小队列)
固定容量队列(比如最多存 100 个任务)必须同步控制“已满”和“已空”两种状态,否则生产者可能永远等不到 notify,或者消费者被卡死。
典型错误:只用一个 std::condition_variable,靠同一个谓词判断空/满——这会导致信号混淆。正确做法是两个条件变量,或一个变量 + 更精细的谓词逻辑。
- 推荐方案:一个
std::condition_variable,但谓词分别检查queue.size() (生产者等)和 <code>!queue.empty()(消费者等) - 生产者在
push前检查容量,满则wait();消费者在pop前检查非空,空则wait() - 注意:
size()本身也要在锁内调用,否则读到的值可能瞬间过期
调试时看到 std::system_error: Operation not permitted 怎么办
这个错误几乎都发生在 std::condition_variable::wait() 调用时,std::unique_lock 没有正确持有锁,或者锁已被 move 出作用域。
最常见原因:把锁对象声明在 if 分支里、提前 unlock()、或用了 std::lock_guard(它不支持手动 unlock,也不支持传给 wait())。
- 必须用
std::unique_lock,且确保 wait 调用时 lock 处于 locked 状态 - 别写
std::unique_lock<:mutex> lk(mtx); lk.unlock(); cv.wait(lk, ...)</:mutex>—— unlock 后 wait 会直接崩溃 - 编译时加
-D_GLIBCXX_DEBUG(GCC)可捕获部分锁状态异常,但不能替代逻辑检查
线程模型本身不难,难的是每个临界区的边界、每个 notify 的时机、每个 wait 的条件是否真正覆盖了所有竞争路径。漏掉一次重检、多一次 unlock、少一次 notify,程序就可能卡死或崩得毫无征兆。










