直接用裸指针写无锁队列大概率出错,因为std::atomic不支持对裸指针做原子读-改-写(如CAS),核心操作必须作用于std::atomic等原子类型,否则易因内存序、ABA、未对齐访问等导致崩溃或静默错误。

为什么直接用裸指针写无锁队列大概率会出错
因为 std::atomic 不支持对裸指针做原子读-改-写(如 CAS),而无锁队列的核心操作(比如 compare_exchange_weak)必须作用于原子类型。你声明 Node* head;,哪怕后续用 __atomic_compare_exchange 手动调用,也极易因内存序、重排序、ABA 问题或未对齐访问导致崩溃或静默错误。
常见错误现象包括:队列偶尔丢节点、消费者卡死、segmentation fault 在 load(memory_order_acquire) 后立即解引用、多线程下 head->next 返回已释放内存地址。
- 必须把指针本身包装进
std::atomic<node></node>或使用std::atomic<uintptr_t></uintptr_t>+ 手动 cast(不推荐) - 所有指针读写必须通过
load()/store()/exchange()/compare_exchange_weak()进行 - 不能在非原子变量上做“先读 head,再读 head->next”这种非原子链式访问
单生产者单消费者(SPSC)场景下最简可行实现
SPSC 是唯一能避开 ABA 和内存回收难题的起点。此时无需 Hazard Pointer 或 RCU,只需两个 std::atomic<node></node> + 内存序控制。
关键点在于:生产者只改 tail,消费者只改 head;每次 push/pop 都用 memory_order_relaxed 读对方指针,但用 memory_order_release 存自己的更新,并用 memory_order_acquire 读自己刚写的节点数据。
struct Node {
int data;
std::atomic<node> next{nullptr};
};
<p>struct SPSCQueue {
std::atomic<node>> head{new Node{}};
std::atomic<node>> tail{head.load()};</node></node></p>
<pre class="brush:php;toolbar:false;">void push(int x) {
Node* n = new Node{x};
Node* t = tail.load(std::memory_order_relaxed);
t->next.store(n, std::memory_order_release); // 确保 data 先写入
tail.store(n, std::memory_order_relaxed);
}
bool pop(int& x) {
Node* h = head.load(std::memory_order_relaxed);
Node* t = tail.load(std::memory_order_relaxed);
Node* n = h->next.load(std::memory_order_acquire);
if (n == nullptr) return false;
x = n->data;
head.store(n, std::memory_order_relaxed);
delete h; // 安全:只有本线程修改 head,且 h 已被移出链
return true;
}
};
注意:这个版本仅适用于严格 SPSC。若多个生产者同时 push,tail->next.store() 会竞争冲突,必须升级为 CAS 循环。
多生产者多消费者(MPMC)必须解决的三个硬伤
MPMC 下,push 和 pop 都要并发修改 head 或 tail,裸指针加原子操作仍不够——你会立刻撞上:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
-
ABA 问题:线程 A 读到
tail == p,被抢占;线程 B 把 p 出队又入队,tail又回到 p;A 恢复后 CAS 成功,但语义已错 -
内存回收困境:谁来
delete节点?CAS 失败的线程可能持有已失效指针,直接 delete 会 double-free -
tail 更新竞争:两个生产者同时读到同一
tail,都试图写tail->next,后者覆盖前者,造成节点丢失
标准解法是引入「带版本号的指针」(std::atomic<uint64_t></uint64_t> 存 (ptr )防 ABA,再配合 Hazard Pointer 或 epoch-based reclamation 延迟释放节点。但这些机制本身代码量远超队列逻辑,且易出错。
实际项目中,除非吞吐压测证明 std::queue<t></t> + std::mutex 真成瓶颈(通常不会),否则别碰 MPMC 无锁队列。
别自己造轮子:优先用成熟实现
Linux 的 liburcu、Facebook 的 Folly::MPMCQueue、Intel TBB 的 tbb::concurrent_queue 都经过严苛测试。它们处理了缓存行伪共享、NUMA 感知分配、中断安全、调试支持等你没意识到的问题。
例如 Folly::MPMCQueue 默认用数组+索引而非链表,规避指针管理;内部用 std::atomic<size_t></size_t> + 内存屏障组合,不暴露裸指针给用户;还提供 blocking_pop 和 try_emplace 等实用接口。
真正难的从来不是“怎么让 CAS 成功”,而是“怎么确保失败路径不泄漏资源、不破坏 invariant、不拖慢成功路径”。这些边界条件,调试起来比写初始版本花十倍时间。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










