不能直接用 std::atomic 包裹指针实现无锁链表,因其仅能原子读写指针本身,而插入/删除需“读头→构造→更新”三步非原子操作,且存在 ABA 问题和 use-after-free 风险;须结合 compare_exchange_weak、版本号标记及内存回收机制(如 hazard pointer)才能安全实现。

为什么不能直接用 std::atomic 包裹指针来实现无锁链表
因为 std::atomic<t></t> 只能原子地读写指针本身,但链表的插入/删除操作天然需要「读取当前头、构造新节点、更新头指针」三步——这三步合起来不是原子的。更关键的是,如果两个线程同时执行 head.load() 得到相同旧值,再各自 head.store(),就会发生 ABA 问题或丢失更新。裸用 std::atomic<node></node> 只能做 lock-free stack 的 push(单点修改头),无法安全支持任意位置插入或遍历中删除。
必须用 compare_exchange_weak + ABA 防护才能做基础插入
无锁插入的核心是循环重试:读当前头 → 构造新节点并设置 next 指向旧头 → 用 compare_exchange_weak 原子地把头从旧值换成新节点。失败就重来。但这里有个陷阱:如果旧头被弹出、释放、又恰好分配给新节点(地址复用),compare_exchange_weak 会误判为“没变过”,导致逻辑错误(ABA)。解决办法不是加锁,而是给指针带上版本号:
struct Node {
int data;
std::atomic<uint64_t> next_with_tag; // 低32位存指针,高32位存版本号
};
每次修改都递增版本号,compare_exchange_weak 同时比对指针和版本。标准库没直接提供带 tag 的原子指针,得手写封装或用 std::atomic<std::pair<Node*, uint32_t>>(注意:std::pair 必须是 trivially copyable,且平台需支持 128-bit CAS)。
遍历时不能简单解引用 next,否则触发 use-after-free
无锁结构最大的隐形成本不是写,是读——因为你永远不知道某个节点是否刚被其他线程 unlink 并 delete。常见错误是这样遍历:
立即学习“C++免费学习笔记(深入)”;
auto cur = head.load();
while (cur) {
process(cur->data);
cur = cur->next.load(); // 危险!cur 可能在上一行后就被 delete
}
正确做法必须配合内存屏障与安全发布机制,例如:使用 hazard pointer 或 epoch-based reclamation(EBR)。最简可行方案是让所有节点生命周期由使用者管理(即不自动 delete),只做逻辑删除(如设 deleted 标志),遍历时跳过;或者强制要求调用方在遍历前「冻结」链表(比如用一个全局读写锁——那就不是无锁了)。真正的无锁遍历没有银弹,hazard pointer 实现本身就要几十行代码,且增加每线程局部存储开销。
别自己造轮子,先看 folly::AtomicLinkedList 和 moodycamel::ConcurrentQueue 的设计
工业级无锁链表几乎都基于 Michael-Scott 队列或 Harris list 改进,它们用标记指针(tagged pointer)+ 冗余检查 + 多重 CAS 来处理删除。你自己实现时最容易翻车的点是:忘记内存序(该用 memory_order_acquire 的地方用了 relaxed)、漏掉编译器屏障(std::atomic_thread_fence)、或在析构时没等所有读者退出。除非你正在写内核模块或高频交易中间件,否则优先用成熟库。哪怕只是学习,也建议先读懂 folly/AtomicLinkedList.h 里 insertHead 如何用 __atomic_compare_exchange 和 std::atomic_thread_fence 配合完成一次插入——那几行代码背后是二十多年论文演进。


















