std::atomic不能直接操作指针成员是因为它不支持对含next指针的自定义节点结构做原子读写,即使满足平凡复制条件,load/store也仅保证内存顺序而非多步操作原子性;真正可行的是用std::atomic配合compare_exchange_weak实现cas循环头插,且需正确处理内存释放与aba问题。

为什么不能直接用 std::atomic 操作指针成员
因为 std::atomic 不支持对自定义结构体(比如含 next 指针的节点)做原子读写,除非该结构体是标准布局且所有成员可平凡复制——但即使满足,load()/store() 也只保证内存顺序,不保证多步操作(如“读旧头→构造新节点→写新头”)的原子性。常见错误是以为给 Node* 加 std::atomic 就能安全 push,结果在多线程下看到 next 是随机值或崩溃。
真正可用的是 std::atomic<node></node>,它封装了对裸指针的原子读写,但必须配合 compare_exchange_weak 才能实现无锁插入。
push_front 必须用 CAS 循环重试
单向链表头插的无锁核心就是“读当前头 → 构造新节点并设 next → 原子比较并交换头指针”。一旦期间有其他线程修改了头,CAS 失败,就得重试。
-
head必须声明为std::atomic<node></node>,初始值为nullptr - 每次
new Node(val)后,必须先设置node->next = old_head,再尝试head.compare_exchange_weak(old_head, node) - 不能用
compare_exchange_strong:失败可能因 ABA 或伪失败,weak版本更轻量,配合循环即可 - 注意
old_head是引用类型,在循环中会被 CAS 更新为当前真实头值
void push_front(int val) {
Node* node = new Node(val);
Node* old_head = head.load();
while (!head.compare_exchange_weak(old_head, node)) {
node->next = old_head; // 重试前更新 next,防止指向已释放节点
}
}
pop_front 需要处理内存释放与 ABA 问题
无锁 pop 的难点不在逻辑(读头→CAS换头→返回旧头),而在于:旧头节点可能被其他线程刚释放,当前线程却还在访问它的 next 字段;或者 ABA 问题导致 CAS 成功但语义错误(头从 A→B→A,你误以为没变)。
简单场景(单生产者单消费者、或允许延迟释放)可绕过 ABA:
- 用
std::atomic<uint64_t></uint64_t>给每个节点加版本号,把指针和版本打包成uint128_t(需std::atomic<__int128></__int128>或自定义结构 +compare_exchange_weak) - 更实际的做法:用 hazard pointer 或 epoch-based reclamation(EBR),但实现复杂;若仅测试/学习,可先禁用 pop 的并发调用,或改用带锁的 pop
- 最简 pop 实现(不保证强线程安全):
Node* old = head.load(); if (old && head.compare_exchange_strong(old, old->next)) return old;—— 但必须确保没有其他线程正在析构old
构造节点时避免 new 引发的性能与异常风险
频繁 new 在高并发下会成为瓶颈,且可能抛异常打断 CAS 流程。工业级无锁链表通常预分配节点池。
- 用
std::vector<:unique_ptr>></:unique_ptr>预分配一批节点,用std::atomic_size_t管理空闲索引 - 或使用
std::pmr::polymorphic_allocator搭配内存池,避免系统堆竞争 - 若必须用
new,至少捕获异常并在 CAS 失败后delete node,否则内存泄漏 - 注意:节点构造函数不能抛异常,否则 CAS 中途失败时无法回滚状态
无锁链表真正的复杂点从来不在 push 逻辑本身,而在内存生命周期管理——谁释放、何时释放、能否被安全访问,这些决定了它到底是不是真的“无锁可用”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











