线程安全队列:用std::optional替代std::shared_ptr是否可行?
线程安全队列:原书实现与std::optional改写分析
原书(《C++ Concurrency in Action》2012版)的线程安全队列实现
原书采用存储std::shared_ptr<T>的方式实现线程安全队列,核心代码如下:
template<typename T> class ThreadSafeQueue { private: mutable std::mutex mut; std::queue<std::shared_ptr<T>> data_queue; std::condition_variable cv; public: ThreadSafeQueue() {} void wait_and_pop(T& val) { std::unique_lock<std::mutex> lk(mut); cv.wait(lk, [this] { return !data_queue.empty(); }); val = std::move(*data_queue.front()); data_queue.pop(); } bool try_pop(T& val) { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) { return false; } val = std::move(*data_queue.front()); data_queue.pop(); return true; } std::shared_ptr<T> wait_and_pop() { std::unique_lock<std::mutex> lk(mut); cv.wait(lk, [this] { return !data_queue.empty(); }); std::shared_ptr<T> res = data_queue.front(); data_queue.pop(); return res; } std::shared_ptr<T> try_pop() { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) { return std::shared_ptr<T>(); } std::shared_ptr<T> res = data_queue.front(); data_queue.pop(); return res; } void push(T val) { std::shared_ptr<T> data(std::make_shared<T>(std::move(val))); std::lock_guard<std::mutex> lk(mut); data_queue.push(data); cv.notify_one(); } bool empty() const { std::lock_guard<std::mutex> lk(mut); return data_queue.empty(); } }; // class ThreadSafeQueue
基于std::optional的改写实现
你用std::optional<T>替代空shared_ptr的语义,直接存储T的改写版本代码如下:
template<typename T> class ThreadSafeQueue { private: mutable std::mutex mut; std::queue<T> data_queue; std::condition_variable cv; public: ThreadSafeQueue() {} void wait_and_pop(T& val) { std::unique_lock<std::mutex> lk(mut); cv.wait(lk, [this] { return !data_queue.empty(); }); val = std::move(data_queue.front()); data_queue.pop(); } bool try_pop(T& val) { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) { return false; } val = std::move(data_queue.front()); data_queue.pop(); return true; } T wait_and_pop() { std::unique_lock<std::mutex> lk(mut); cv.wait(lk, [this] { return !data_queue.empty(); }); T res = std::move(data_queue.front()); data_queue.pop(); return res; } std::optional<T> try_pop() { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) { return std::optional<T>(); } std::optional<T> res = std::move(data_queue.front()); data_queue.pop(); return res; } void push(T val) { std::lock_guard<std::mutex> lk(mut); data_queue.push(std::move(val)); cv.notify_one(); } bool empty() const { std::lock_guard<std::mutex> lk(mut); return data_queue.empty(); } }; // class ThreadSafeQueue
实现合理性与潜在问题分析
合理性部分
你的改写有不少符合现代C++实践的优点:
- 语义更清晰:用
std::optional<T>表示try_pop的空结果,比返回空shared_ptr更直观,符合现代C++错误处理的风格。 - 性能提升符合预期:直接存储
T避免了shared_ptr的引用计数开销,锁内操作的内存成本更低,基准测试的1.3倍性能提升是合理的。 - 接口优化:
wait_and_pop直接返回值的方式,避免了引用传递可能带来的线程安全隐患,符合线程安全代码的最佳实践。
潜在问题与遗漏点
1. 核心异常安全缺陷
原书使用shared_ptr的关键优势是锁内操作无异常:
push时在锁外完成std::make_shared(内存分配+对象构造),锁内仅执行shared_ptr的拷贝(noexcept操作),完全避免了锁内异常导致的队列状态不一致。- 返回
shared_ptr的wait_and_pop/try_pop操作,仅涉及shared_ptr的拷贝(noexcept),不会因对象构造/移动抛出异常。
而你的改写版本存在两处严重的异常安全问题:
wait_and_pop()返回T的版本:若T res = std::move(data_queue.front());的移动构造抛出异常,代码仍会执行data_queue.pop();,导致队列元素被移除但值未成功取出,直接造成元素丢失。try_pop()返回std::optional<T>的版本:同理,若std::optional<T> res = std::move(data_queue.front());中T的移动构造抛异常,元素会被pop但res未有效构造,同样导致元素丢失。
2. 锁持有时间变长
原书版本的push操作在锁外完成对象构造,锁内仅执行极快的shared_ptr入队操作;而你的push操作中,锁内的data_queue.push(std::move(val))若涉及T的移动构造(若T的移动构造非noexcept且耗时),会导致锁持有时间变长,增加线程竞争概率,高并发场景下可能抵消部分性能优势。
3. wait_and_pop(T& val)的固有隐患(非改写新增)
这个接口无论原书还是你的实现都存在问题:若val = std::move(data_queue.front());的移动赋值抛出异常,队列中的元素状态会变为有效但未指定,且仍保留在队列中,后续线程取出该元素时可能触发未定义行为。
优化建议
- 修复异常安全问题:
- 若要求
T的移动构造为noexcept,可保留当前实现;若需支持移动构造可能抛异常的类型,可参考原书思路,在锁外完成T的构造/移动,锁内仅存储已构造好的对象(比如结合std::optional与shared_ptr的优势)。
- 若要求
- 缩短锁持有时间:
修改push的参数为右值引用并提供重载,让对象的移动尽可能在锁外完成:void push(T&& val) { std::lock_guard<std::mutex> lk(mut); data_queue.push(std::move(val)); cv.notify_one(); } void push(const T& val) { std::lock_guard<std::mutex> lk(mut); data_queue.push(val); cv.notify_one(); }
内容的提问来源于stack exchange,提问作者Niccolò Tiezzi
相关产品推荐
相关产品推荐

