直接用 std::atomic 操作指针不等于线程安全,因无法解决 aba 问题和内存释放时机;需配合 std::shared_ptr 原子操作、正确内存序及延迟回收机制。

为什么直接用 std::atomic 操作指针不等于线程安全
很多人以为只要把链表节点的 next 成员声明为 std::atomic<node></node>,再用 load()/store() 就能实现无锁插入——这是典型误解。真正的难点不在原子读写,而在「修改链表结构」时的 ABA 问题和内存释放时机。比如:线程 A 读到 head 指向节点 X,线程 B 删除 X 并重用其内存分配新节点 Y,地址相同;线程 A 继续用 CAS 比较并交换,误认为链表未变,导致逻辑错误或崩溃。
必须配合 std::atomic<:shared_ptr>></:shared_ptr> 或 Hazard Pointer
裸指针 + std::atomic<node></node> 无法解决内存生命周期管理问题。C++11 起推荐用 std::atomic<:shared_ptr>></:shared_ptr>(注意:std::shared_ptr 的原子操作是特化支持的),它天然携带引用计数,避免提前释放。但要注意:
-
std::shared_ptr的原子操作(如atomic_load、atomic_store)必须显式调用,不能直接对std::atomic<:shared_ptr>></:shared_ptr>对象用=赋值 - 插入时需用
compare_exchange_weak循环重试,直到成功更新头指针 - 所有节点构造必须通过
std::make_shared,否则shared_ptr无法正确管理原始指针
示例插入逻辑:
auto new_node = std::make_shared<node>(value);
do {
auto old_head = atomic_head.load();
new_node->next = old_head;
} while (!atomic_head.compare_exchange_weak(old_head, new_node));</node>
删除操作无法真正「无锁」,只能做到 lock-free 插入 + 延迟回收
单向链表的删除需要修改前驱节点的 next,而你无法在不遍历的前提下定位前驱——这意味着要么放弃 O(1) 删除,要么引入辅助数据结构(如带前驱指针的双向链表,但这已不是「单向」)。实际中,多数无锁单向链表只支持线程安全的头插(push_front),删除(pop_front)可实现,但任意节点删除不可行。若真需删除任意节点,必须接受以下现实:
- 要么改用
Hazard Pointer或RCU等更重的内存回收机制 - 要么放弃「任意节点删除」,只提供「标记删除 + 后台清理」(即逻辑删除,后续遍历时跳过)
- 使用
std::atomic_flag配合自旋等待做轻量级删除同步,但这已不算严格无锁(lock-free)
编译和内存序别踩 memory_order_relaxed 的坑
无锁编程里,内存序选错会导致看似正常、实则偶发崩溃。对头插场景:
-
load()头指针必须至少用memory_order_acquire(保证后续读next不被重排到 load 前) -
store()新头指针必须至少用memory_order_release(保证之前对新节点的初始化不被重排到 store 后) -
compare_exchange_weak默认使用memory_order_seq_cst,虽安全但性能略低;可显式指定为memory_order_acq_rel,兼顾正确性与效率
漏掉 acquire/release,多核下可能读到未完全构造好的节点内容,尤其在 ARM/PowerPC 上更容易暴露。
无锁链表的复杂点从来不在代码行数,而在内存模型理解深度和回收机制选择——写完几行 CAS,不代表你真控制了内存命运。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











