不能直接用 std::atomic 操作链表节点指针,因其仅保证指针值原子性,不保证所指内存线程安全,且无法解决 aba 问题和节点状态协同更新需求。

为什么不能直接用 std::atomic<t></t> 操作链表节点指针
因为 std::atomic<t></t> 只保证指针值的原子读写,不保证对它所指向内存的修改是线程安全的;更关键的是,链表插入/删除需要「比较并交换」整个节点状态(比如 prev→next 和 cur→next 的协同更新),单靠指针原子性无法避免 ABA 问题或中间态撕裂。实际中你很快会遇到 load() returned nullptr but dereference crashed 或无限循环的 CAS 失败——这不是原子性不够,而是操作语义不匹配。
正确做法是把节点本身设计为可原子更新的单元,常见模式是让节点包含 std::atomic<node> next</node> 字段,并用 std::atomic_compare_exchange_weak 配合 fetch_add 或 fetch_or 等辅助标记位(如已删除、正在删除)。
- 不要把裸指针塞进
std::atomic<void></void>后手动 reinterpret_cast —— 类型擦除丢失了对象生命周期和对齐保证 - 避免在
next上反复load(memory_order_acquire)再判断,应合并为一次带条件的 CAS 尝试 - 所有对
next的写入必须用store(..., memory_order_release)或更强序,否则其他线程可能看到部分更新的指针+脏数据
如何用 std::atomic<node></node> 实现无锁栈(LIFO)插入
无锁栈比队列简单,适合入门理解原子指针的核心模式:CAS 循环 + head 指针双阶段更新。核心不是“快”,而是“每次尝试只改一个位置、失败就重试”。
struct Node {
int data;
std::atomic<node> next{nullptr};
};
<p>class LockFreeStack {
std::atomic<node>> head{nullptr};
public:
void push(int x) {
Node node = new Node{x};
Node* 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></p></node>
-
compare_exchange_weak必须用do-while包裹:它可能因虚假失败返回 false,但expected已被更新为当前值,可直接重试 -
node->next.store(expected, ...)用relaxed是安全的,因为后续的 CAS release 会建立释放序列 - 内存泄漏风险:没有 GC 时,
pop()必须配合 hazard pointer 或 epoch-based reclamation,不能直接delete
为什么无锁队列(FIFO)比栈难得多
栈只需维护 head;队列需同时协调 head 和 tail,且 tail 可能远落后于 head,导致典型的“tail chase”现象——一个线程在追 tail,另一个刚更新完 tail 就被第三个线程覆盖。标准解法是 Michael-Scott 队列,但它要求 next 字段支持双字 CAS(128-bit),而 x86-64 上 std::atomic<:pair node>></:pair> 并不天然支持(需 __int128 或平台内置函数)。
- 别硬凑
std::atomic<uint64_t></uint64_t>存两个指针:低 32 位存 ptr,高 32 位存 tag —— 在 64 位系统上地址可能超 4GB,且缺乏原子双字操作保障 - Linux 上可用
__atomic_compare_exchange_16(GCC),但 Windows MSVC 对 128-bit CAS 支持有限,跨平台慎用 - 如果业务允许近似 FIFO,用多个无锁栈分片 + 中央调度器,反而比单个无锁队列更稳定
真正容易被忽略的内存回收陷阱
无锁结构最常崩在内存释放时机:一个节点被逻辑删除(如 next 设为 marked 指针),但另一线程正通过旧 next 访问它,此时若直接 delete,就是 UAF(use-after-free)。这不是原子指针的问题,而是无锁算法固有的内存管理责任。
- 不要依赖
std::shared_ptr:引用计数操作非原子,且weak_ptr::lock()无法保证观察到最新next链 - Hazard pointer 实现复杂但可控:每个线程注册当前正在访问的节点指针,回收器只删那些不在任何 hazard list 中的节点
- RCU 更轻量但要求读多写少,且 Linux kernel RCU 不适用于用户态;liburcu 可用,但需注意 signal handler 安全与 grace period 控制
没配内存回收机制的无锁列表,跑得越快,崩溃越随机。先跑通逻辑,再补回收——但千万别跳过这步。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











