不能直接用 std::atomic 操作链表节点,因其仅保证指针读写原子性,不保证“读指针→改next→写回”整套操作原子性,易引发 aba 或悬空指针;正确做法是用 std::atomic 配合 cas 循环,并引入延迟内存回收机制。

为什么不能直接用 std::atomic<t></t> 操作链表节点
因为 std::atomic<t></t> 只保证指针本身的读写原子性,不保证“读旧指针 → 修改其 next 字段 → 写回”这一整套操作的原子性。典型错误是:线程 A 读到 head 指向 node1,线程 B 同时把 node1 的 next 改成 node2 并 CAS 更新 head,A 接着用已失效的 node1 地址去改 next,造成 ABA 或悬空指针。
正确做法是用 std::atomic<node></node> 存储整个节点指针,并配合 compare_exchange_weak 实现原子更新。但注意:节点本身不能被任意释放——必须等所有可能正在访问它的线程都退出后才能回收,否则就是 use-after-free。
- 所有修改操作(push/pop)必须用 CAS 循环重试,不能假设一次成功
- 节点内存不能用
delete立即释放,需引入内存回收机制(如 hazard pointer 或 epoch-based reclamation),简单实现可先用对象池或禁止删除 -
next字段必须声明为std::atomic<node></node>,否则并发读写会触发未定义行为
如何安全地实现 push_front(头插)
头插是最容易实现的无锁操作,只需 CAS 更新 head 指针。关键在于:新节点的 next 必须在 CAS 前设好,且不能被其他线程干扰。
struct Node {
int data;
std::atomic<node> next{nullptr};
};
<p>class LockFreeSinglyList {
std::atomic<node>> head{nullptr};
public:
void push_front(int val) {
Node node = new Node{val, nullptr};
Node* expected;
do {
expected = head.load();
node->next.store(expected, std::memory_order_relaxed);
} while (!head.compare_exchange_weak(expected, node));
}
};</node></p></node>
- CAS 失败时,
expected被自动更新为当前 head 值,无需手动赋值 -
node->next.store(expected, ...)必须在每次循环内执行,防止用过期的 old head - 用
std::memory_order_relaxed对next赋值足够,因为后续 CAS 本身带 acquire/release 语义
pop_front 为什么必须处理空链表和内存安全
pop 比 push 复杂:既要读 head,又要读 head→next,还要原子地把 head 替换为 head→next。若不加防护,可能读到已释放节点的 next 字段(哪怕 head 还没被别的线程改掉)。
最简安全版(不回收内存)如下:
int pop_front() {
Node* old_head = head.load();
while (old_head != nullptr) {
Node* next = old_head->next.load();
if (head.compare_exchange_weak(old_head, next)) {
int ret = old_head->data;
// 注意:这里不能 delete old_head!
return ret;
}
// CAS 失败,old_head 已被其他线程更新,重试
}
throw std::runtime_error("list is empty");
}
- 必须检查
old_head == nullptr,否则old_head->next触发空指针解引用 - 即使 CAS 成功,也不能立刻
delete old_head——别的线程可能刚读到这个指针正准备用它做 CAS - 如果业务允许“只增不删”,可用对象池预分配节点,pop 后放回池中;否则必须引入延迟回收(如
hazard pointer库)
实际使用中最容易被忽略的三个点
无锁链表不是“把指针换成 atomic 就完事”。真正卡住人的地方往往不在逻辑,而在边界和内存。
- 构造函数/析构函数里不能有依赖链表状态的操作——比如析构时遍历并
delete所有节点,这在并发下必然崩溃 - 所有节点生命周期必须独立于链表结构:要么全程不 delete,要么用专门的回收器(
libcds或moodycamel::ConcurrentQueue的回收策略可参考) -
std::atomic_thread_fence在多数场景不需要,CAS 本身已提供足够序;但若混用非原子字段(比如给 Node 加个统计计数器),就得小心 memory order 配合
写完之后跑 TSAN(ThreadSanitizer)比跑单元测试还重要——它能直接揪出漏掉的原子操作或 data race。没有 TSAN 验证的无锁代码,基本等于没写。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











