跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

C++实现简单的线程安全队列 _ mutex与condition_variable封装【详解】

直接用 std::queue 加 std::mutex 不够,因消费者需被唤醒而非忙等,必须配合 std::condition_variable 实现等待/通知机制,且 wait() 需用 std::unique_lock 并以 while 循环检查条件防虚假唤醒。 为什么直接用
std::queue
加
std::mutex
还不够? 因为消费者线程在队列为空时不能干等,得“被唤醒”;生产者入队后要主动通知等待的消费者。只锁不通知,会导致忙等待或死锁。
std::condition_variable
就是干这个的——它必须和
std::mutex
配合使用,且只能在已加锁状态下调用
wait()
。
wait()
会自动释放锁、挂起线程,被
notify_one()
或
notify_all()
唤醒后重新抢锁 必须用 while 循环检查条件(不是 if),防止虚假唤醒 别在锁外调用
notify_*
:虽然通常不报错,但语义上应确保通知时消费者确实已在 wait 中(或至少已进入 wait 准备阶段) 如何封装一个可拷贝、支持移动的线程安全队列? 核心是把
std::queue
、
std::mutex
、
std::condition_variable
包进类里,并控制所有访问路径。注意:队列本身不支持拷贝,但封装类可以提供拷贝构造(深拷贝内部数据)和移动语义。 构造函数中初始化
m_queue
、
m_mutex
、
m_cv
push()
加锁 → 插入 →
notify_one()
(唤醒一个消费者,更轻量)
pop()
加锁 → while 检查非空 → 取出 → 返回值(注意:C++17 起可用
std::optional
表达失败,否则建议用引用参数 + bool 返回值) 拷贝构造需加锁遍历原队列并逐个 push(性能差,慎用);移动构造直接接管资源,把原对象置为空
template class ThreadSafeQueue { private: std::queue m_queue; mutable std::mutex m_mutex; std::condition_variable m_cv; public: void push(T item) { std::lock_guard lock(m_mutex); m_queue.push(std::move(item)); m_cv.notify_one(); // 通知一个等待中的 pop } bool try_pop(T& item) { std::lock_guard lock(m_mutex); if (m_queue.empty()) return false; item = std::move(m_queue.front()); m_queue.pop(); return true; } T wait_and_pop() { std::unique_lock lock(m_mutex); m_cv.wait(lock, [this]{ return !m_queue.empty(); }); T item = std::move(m_queue.front()); m_queue.pop(); return item; } };
wait_and_pop()
为什么要用
std::unique_lock
而不是
std::lock_guard
? 因为
std::condition_variable::wait()
必须传入
std::unique_lock
—— 它需要在挂起前临时释放锁,并在唤醒后重新获取锁。
std::lock_guard
不支持中途解锁,没法满足这个契约。
std::unique_lock
更灵活:可延迟加锁、可手动
unlock()
、可转移所有权 不要在
wait()
后再手动
lock()
:它已经帮你处理好了 如果想避免阻塞等待,可用
wait_for()
或
wait_until()
,配合超时逻辑 实际使用中最容易漏掉的三个细节 很多看似正确的代码在线程压测下崩溃,往往栽在这几个点上: C知道 CSDN推出的一款AI技术问答工具 下载 立即学习 “ C++免费学习笔记(深入) ”; 没用 while 循环检查条件:虚假唤醒导致
front()
访问空队列 → 未定义行为 在
pop()
中返回局部对象(比如直接
return m_queue.front()
)而没
std::move
:触发冗余拷贝,且可能延长锁持有时间 多个 condition_variable 共享同一个 mutex 但没区分等待条件:比如同时有“非空”和“非满”两个条件,必须用不同 cv 或在谓词中显式区分 线程安全队列的边界其实很薄:锁保护的是结构一致性,但值语义(比如移动后原对象是否有效)仍由 T 决定。如果 T 的移动构造函数抛异常,整个
push()
就可能破坏队列状态——这点常被忽略。

相关文章