不能直接用std::shared_ptr做无锁队列节点指针,因其引用计数更新非原子、析构不可控,破坏lock-free保证;正确做法是裸指针+std::atomic配合内存回收机制。

为什么不能直接用 std::shared_ptr 做无锁队列的节点指针
因为 std::shared_ptr 的引用计数操作本身不是原子的(内部计数器更新可能涉及锁或 CAS 循环+内存屏障组合),在高并发下会成为瓶颈,且其析构可能触发任意代码(比如自定义 deleter),破坏无锁结构的“wait-free”或“lock-free”保证。真正无锁队列必须所有节点管理完全基于原子操作,包括指针本身的读写和内存生命周期控制。
常见错误是把 std::atomic<:shared_ptr>></:shared_ptr> 当成“无锁”,但这是伪无锁——load()/store() 虽原子,而 operator= 或 exchange() 可能隐式调用 shared_ptr 的拷贝构造/赋值,触发非原子计数更新。
- 正确做法:用裸指针 +
std::atomic<t></t>,配合 Hazard Pointer 或 RCU 等机制管理内存回收 - 更轻量的选择:使用
std::atomic<:uintptr_t></:uintptr_t>存储指针地址,手动做指针-整数转换(需确保地址对齐、无 ASLR 干扰) - 工业级方案(如 folly::MPMCQueue)直接禁用动态分配,用预分配 slab + 位图标记,彻底规避释放时机问题
std::atomic 的 compare_exchange_weak 在入队时怎么写才不丢任务
入队本质是 CAS 更新 tail 指针,但必须处理 ABA 问题——比如 tail 被其他线程弹出又重用同一地址,导致 CAS 成功却逻辑错误。单纯用 compare_exchange_weak 对裸指针操作不可靠。
典型错误写法:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
while (!tail.load()->next.compare_exchange_weak(nullptr, new_node)) {
tail = tail.load()->next;
} 这段代码在多生产者场景下会因 ABA 和竞态丢失节点。
- 必须给每个节点附加版本号(versioned pointer),例如用
std::atomic<uint64_t></uint64_t>存(ptr ,CAS 时同时比对指针和版本 - 入队循环里,每次 load 后要验证当前 tail 是否仍有效(比如检查其
next是否为nullptr,再尝试 CAS;失败则重试) - 避免在 CAS 前做任何可能阻塞的操作(如 new 分配),否则线程挂起期间 tail 可能被改写,导致后续 CAS 失败后无法恢复一致状态
出队时如何安全读取并释放节点内存而不崩溃
出队最危险的是:读到一个非空 next 指针,刚准备 CAS 更新 head,该节点却被另一个消费者提前 delete 了——造成 use-after-free。
这不是原子指令能解决的问题,必须引入内存回收协议。Hazard Pointer 是最常用且适合任务队列的方案:每个线程维护一组正在访问的指针(hazard pointers),回收前扫描所有线程的 hazard list,确认无引用再 delete。
- 不要在出队 CAS 成功后立刻
delete node,必须先 publish 到 hazard list,再 load head->next,最后 unpublish 并延迟回收 - 可以复用节点内存(对象池),避免频繁 new/delete;但要注意构造/析构语义——任务对象应支持 placement new 和显式 destructor 调用
- 若用
std::memory_order_relaxed读 head,必须搭配 acquire-release 配对(如 head 更新用memory_order_acq_rel),否则编译器/CPU 重排可能导致读到未初始化的任务数据
实际项目中建议绕开手写无锁队列的三个理由
除非你明确压测证明标准队列(如 std::queue + mutex)成了瓶颈,且延迟毛刺不可接受,否则不值得自己实现。
- Linux 上
pthread_mutex_t在无竞争时是 futex,开销远低于多数人预期;glibc 的std::mutex也做了类似优化 - folly::ProducerConsumerQueue 和 moodycamel::ConcurrentQueue 经过多年打磨,支持批量操作、缓存行对齐、虚假共享规避,比手写更稳
- 无锁队列调试极其困难:GDB 看不到中间状态,TSAN 对原子操作支持有限,core dump 里往往只显示 segfault,根源却是内存回收时机错乱
真要极致性能,优先考虑减少锁粒度(如分段队列)、换用 ring buffer(固定大小、无内存管理开销)、或者干脆用 channel 模型(如 libmill 或 modern C++ coroutine + awaitable queue)——比无锁指针更易验证正确性。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










