不能直接用 std::atomic 操作链表节点指针,因其仅保证指针读写原子性,不保证解引用+修改的原子性,且存在 ABA 问题;pop_front 更因内存回收难题而难以安全实现,必须借助 Hazard Pointer、RCU 等回收机制。

为什么不能直接用 std::atomic<t></t> 操作链表节点指针
因为 std::atomic<t></t> 只保证指针本身的读写原子性,不保证「读指针 → 解引用 → 修改 next 字段」这一系列操作的原子性。更关键的是:**ABA 问题会悄无声息地破坏链表结构**。比如一个节点 A 被弹出、释放、又恰好被新分配为同一地址,此时用 CAS 判断指针没变就认为状态未变,实际中间已发生过修改。
所以必须用 std::atomic<:uintptr_t></:uintptr_t> 或 std::atomic<uint64_t></uint64_t> 手动拼装带版本号的指针(即「tagged pointer」),或者依赖 std::atomic<t>::compare_exchange_weak</t> 配合内存序与重试逻辑——但后者在无锁单向链表中极难正确处理内存回收,容易出现 use-after-free。
- 不要试图对裸指针做 CAS 后直接
delete节点:其他线程可能正通过旧指针访问它 -
std::shared_ptr不能直接用于无锁结构:引用计数本身不是无锁的,且控制块分配引入额外开销和不确定性 - 即使只实现
push_front,也要考虑多个线程同时 push 导致的 CAS 失败重试路径是否完备
如何安全地实现 push_front(仅插入)
这是唯一能相对干净实现的无锁操作。核心是用 compare_exchange_weak 原子更新 head 指针,但必须确保新节点的 next 字段在 CAS 之前已正确设置,并且整个过程不依赖外部同步。
示例关键片段:
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
struct Node {
int data;
std::atomic<Node*> next{nullptr};
};
<p>std::atomic<Node*> head{nullptr};</p><p>void push_front(int val) {
Node<em> node = new Node{val, nullptr};
Node</em> expected;
do {
expected = head.load();
node->next.store(expected, std::memory_order_relaxed);
} while (!head.compare_exchange_weak(expected, node,
std::memory_order_release, std::memory_order_relaxed));
}-
node->next.store(expected, ...)必须在 CAS 之前完成,且用relaxed即可:只要 CAS 成功,该写入对其他线程可见 - CAS 用
release:保证本线程此前所有写操作(如初始化data)对后续成功读到新 head 的线程可见 - 失败时
expected被自动更新,直接重试,无需手动 reload
为什么 pop_front 几乎必然引入内存回收难题
无锁 pop 需要原子读取 head、原子更新 head、再安全释放原 head 节点——但「释放」这一步无法原子化。一旦 CAS 成功,其他线程可能仍持有旧 head 指针(比如刚进入函数但还没读 next),此时 delete 就是悬垂指针。
常见错误写法:
Node* pop_front() {
Node* old_head = head.load();
if (old_head == nullptr) return nullptr;
Node* next = old_head->next.load();
if (head.compare_exchange_strong(old_head, next)) {
return old_head; // ⚠️ 危险!返回后立即 delete?谁来保证没人用?
}
return nullptr;
}- 返回裸指针等于把内存生命周期管理甩给调用方,违背无锁容器契约
- 即便调用方立刻
delete,也不能阻止其他线程正在执行old_head->next.load() - 真正可行的方案只有:基于 Hazard Pointer、RCU 或 epoch-based reclamation(如
libcds中的gc::HP),但这些都不是“简单”能手写的
实际可用的简化方案:只支持 push_front + 迭代遍历
如果你只需要生产者并发 push、单线程消费(比如日志缓冲、事件队列),那可以放弃 pop,改用「不可变链表头 + 遍历消费 + 批量回收」模式。这样既避开 ABA 和内存回收,又保持 lock-free。
- 消费线程定期读取当前
head,遍历整条链(用普通指针解引用,无需原子) - 遍历完成后,将整条链的节点统一
delete;此时生产者只往头部追加,不影响已遍历部分 - 需配合消费线程的「遍历开始前快照 head」+ 「遍历结束后原子交换 head 为 null」,避免漏项
- 若需多消费者,则必须引入某种全局回收协调机制,复杂度陡增
真正的无锁单向链表不是“把指针换成 atomic 就完事”,而是围绕内存生命周期设计整个协议。想跑通 push 和 pop 都无锁,就得接受引入一套 GC 机制——而这已经超出“简单实现”的范畴。

















