如何使用std::mutex与condition_variable实现经典生产者消费者模型?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1165个文字,预计阅读时间需要5分钟。
由于生产者和消费者在等待对方时不能直接占有锁,否则对方将永远无法获得锁,也无法唤醒你。此时,必须将等待逻辑从临界区中分离出来,使用`std::condition_variable`配合`std::unique_lock`来实现。
以下是修改后的代码片段:
常见错误现象:std::condition_variable::wait() 被调用时没传 std::unique_lock,或者传了但锁已释放;或者忘了在 wait() 的 lambda 条件里检查实际状态(比如队列是否真非空),导致虚假唤醒后直接读空队列崩溃。
- 必须用
std::unique_lock(不能用std::lock_guard),因为wait()会内部释放并重新获取它 - 条件判断必须写在
wait()的第二个参数 lambda 里,且该 lambda 返回true才继续执行,否则继续等 - 所有对共享队列的读写操作,都必须包裹在同一个
std::unique_lock作用域内
如何避免 notify_one() 唤醒错线程?
用两个独立的 std::condition_variable:一个专管“队列有数据可取”(not_empty),另一个专管“队列有空位可放”(not_full)。混用一个变量会导致消费者被 notify_one() 唤醒后发现队列仍为空(生产者还没来得及 push),又得继续等——这不是 bug,但效率低;而用两个变量能精准通知对应角色。
使用场景:缓冲区大小有限(比如固定容量 max_size = 10),需要同时控制上限和下限。
立即学习“C++免费学习笔记(深入)”;
- 生产者 push 前 wait
not_full,push 后 notifynot_empty - 消费者 pop 前 wait
not_empty,pop 后 notifynot_full - 别用
notify_all()——除非你明确需要广播,多数情况notify_one()更轻量
std::queue 在多线程里为什么不能直接用?
std::queue 本身不是线程安全的,它的 empty()、size()、front()、pop() 等操作之间没有原子性。比如你先 if (!q.empty()) 再 q.front(),中间可能已被另一个线程 pop 掉,导致 front() 抛异常或未定义行为。
所以不能把判断和取值拆成两步。正确做法是:在持有锁的前提下,用一个函数完成“检查 + 取值 + 删除”,例如:
bool try_pop(T& item) { std::unique_lock<std::mutex> lock(mtx); if (q.empty()) return false; item = std::move(q.front()); q.pop(); return true; }
- 不要单独调用
q.size()判断容量,改用计数器变量或直接比较q.size() < max_size(但需在锁内) - 如果要用
std::queue::size(),确保它和后续操作在同一个锁保护块里 - 移动语义(
std::move)比拷贝更高效,尤其对大对象
程序退出时消费者还在等怎么办?
主线程结束前没发停止信号,子线程卡在 wait() 上,进程无法正常退出。解决方案是引入一个 std::atomic_bool running{true} 标志,并在所有 wait() 的 lambda 条件中加入 && running,然后在退出前设为 false 并调用 notify_all()。
容易踩的坑:只改标志不 notify,线程永远等下去;或者 notify 了但没加标志检查,导致唤醒后继续进 wait(虚假唤醒叠加逻辑漏洞)。
- 修改条件 lambda:例如
[this]{ return !q.empty() && running; } - 析构或 shutdown 时执行:
running = false; not_empty.notify_all(); not_full.notify_all(); - 每个工作线程末尾要加
join(),否则主线程结束时子线程被强制终止,资源没清理
本文共计1165个文字,预计阅读时间需要5分钟。
由于生产者和消费者在等待对方时不能直接占有锁,否则对方将永远无法获得锁,也无法唤醒你。此时,必须将等待逻辑从临界区中分离出来,使用`std::condition_variable`配合`std::unique_lock`来实现。
以下是修改后的代码片段:
常见错误现象:std::condition_variable::wait() 被调用时没传 std::unique_lock,或者传了但锁已释放;或者忘了在 wait() 的 lambda 条件里检查实际状态(比如队列是否真非空),导致虚假唤醒后直接读空队列崩溃。
- 必须用
std::unique_lock(不能用std::lock_guard),因为wait()会内部释放并重新获取它 - 条件判断必须写在
wait()的第二个参数 lambda 里,且该 lambda 返回true才继续执行,否则继续等 - 所有对共享队列的读写操作,都必须包裹在同一个
std::unique_lock作用域内
如何避免 notify_one() 唤醒错线程?
用两个独立的 std::condition_variable:一个专管“队列有数据可取”(not_empty),另一个专管“队列有空位可放”(not_full)。混用一个变量会导致消费者被 notify_one() 唤醒后发现队列仍为空(生产者还没来得及 push),又得继续等——这不是 bug,但效率低;而用两个变量能精准通知对应角色。
使用场景:缓冲区大小有限(比如固定容量 max_size = 10),需要同时控制上限和下限。
立即学习“C++免费学习笔记(深入)”;
- 生产者 push 前 wait
not_full,push 后 notifynot_empty - 消费者 pop 前 wait
not_empty,pop 后 notifynot_full - 别用
notify_all()——除非你明确需要广播,多数情况notify_one()更轻量
std::queue 在多线程里为什么不能直接用?
std::queue 本身不是线程安全的,它的 empty()、size()、front()、pop() 等操作之间没有原子性。比如你先 if (!q.empty()) 再 q.front(),中间可能已被另一个线程 pop 掉,导致 front() 抛异常或未定义行为。
所以不能把判断和取值拆成两步。正确做法是:在持有锁的前提下,用一个函数完成“检查 + 取值 + 删除”,例如:
bool try_pop(T& item) { std::unique_lock<std::mutex> lock(mtx); if (q.empty()) return false; item = std::move(q.front()); q.pop(); return true; }
- 不要单独调用
q.size()判断容量,改用计数器变量或直接比较q.size() < max_size(但需在锁内) - 如果要用
std::queue::size(),确保它和后续操作在同一个锁保护块里 - 移动语义(
std::move)比拷贝更高效,尤其对大对象
程序退出时消费者还在等怎么办?
主线程结束前没发停止信号,子线程卡在 wait() 上,进程无法正常退出。解决方案是引入一个 std::atomic_bool running{true} 标志,并在所有 wait() 的 lambda 条件中加入 && running,然后在退出前设为 false 并调用 notify_all()。
容易踩的坑:只改标志不 notify,线程永远等下去;或者 notify 了但没加标志检查,导致唤醒后继续进 wait(虚假唤醒叠加逻辑漏洞)。
- 修改条件 lambda:例如
[this]{ return !q.empty() && running; } - 析构或 shutdown 时执行:
running = false; not_empty.notify_all(); not_full.notify_all(); - 每个工作线程末尾要加
join(),否则主线程结束时子线程被强制终止,资源没清理

