无锁单向链表必须用std::atomic管理指针,禁止裸指针读写;push_front需cas循环实现;pop_front无法真正安全删除,须配合内存回收或逻辑删除;必须防范aba问题,如版本号扩展指针。

无锁单向链表必须用 std::atomic 管理指针
裸指针(Node*)在多线程下读写必然引发数据竞争,C++11 起唯一合规做法是用 std::atomic<node></node> 替代。不能只对数据加锁、让 next 指针裸奔——那不是无锁,只是“部分上锁”。
常见错误:把 std::atomic_flag 或自定义 CAS 循环套在普通指针上,结果因编译器重排或缓存不一致导致读到撕裂值(比如高位是旧地址、低位是新地址)。
-
std::atomic<node></node>保证指针读写原子性,且默认使用memory_order_seq_cst(强顺序),初学者可先不调优 - 构造时必须显式初始化为
nullptr,否则未定义行为:std::atomic<node> head{nullptr};</node> - 所有访问都必须通过
.load()/.store()/.compare_exchange_weak(),禁止直接解引用或赋值
push_front 必须用 CAS 循环,不能只试一次
无锁插入本质是“乐观尝试”:读当前头节点 → 构造新节点 → 尝试用 CAS 把 head 改成新节点。失败说明别人抢先改了 head,得重试。
典型陷阱:用 compare_exchange_strong 却没处理失败分支,或者循环里忘了重新读 head,导致无限循环或覆盖他人写入。
- 正确模式是 while 循环 +
compare_exchange_weak:Node* old_head = head.load(); do { new_node->next = old_head; } while (!head.compare_exchange_weak(old_head, new_node)); -
compare_exchange_weak可能伪失败(spurious failure),所以必须配合循环;strong版本在 x86 上没区别,但 ARM 上代价高 - 注意:
old_head是输出参数,失败时会被更新为当前实际值,直接复用即可,不用再load()
删除操作无法真正安全实现,只能标记删除
真正的无锁 pop_front 在单向链表中不可行——因为你无法原子地“读 head → 读 head->next → 写 head = head->next”,中间步骤可能被其他线程干扰,造成 ABA 问题或内存释放后仍被引用。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
现实方案只有两种:要么引入内存回收机制(如 Hazard Pointer 或 RCU),要么放弃物理删除,改用逻辑删除(mark-and-sweep)。
- 简单做法:给节点加一个
std::atomic<bool> marked</bool>,pop_front先 CAS 标记,再 CAS 更新 head;但后续需额外清理线程回收内存 - 更实用的是放弃通用 pop,只提供
try_pop(返回 bool 表示是否成功),并接受“可能漏删”或“需要调用方配合延迟释放” - 绝对不要在无锁链表里直接
delete节点——哪怕你刚 CAS 成功,也可能有另一个线程正拿着旧 head 指针准备 dereference
构造和析构必须规避 ABA 问题
ABA 问题不是理论风险:当线程 A 读到 head == X,被调度挂起;线程 B 把 X 弹出、释放、又新建一个内容相同的新节点 Y 并 push 进去,地址恰好复用 X;A 恢复后 CAS 发现 head 还是 X,就误认为没变,强行覆盖——结果链表断裂。
C++ 标准库不提供内置 ABA 防御,必须手动扩展指针语义。
- 最轻量做法:用
std::atomic<uint64_t></uint64_t>存“指针+版本号”,高位存指针(需对齐保证低比特空闲),低位存计数,每次修改都 bump 版本 - 避免用
std::shared_ptr包裹节点——它内部引用计数非 lock-free,且无法保证原子交换 - 如果业务场景明确“只增不删”,ABA 可忽略;但只要涉及删除或复用内存,就必须处理
无锁链表的复杂度不在代码行数,而在内存生命周期管理。写完 push 不等于做完,没搞定内存回收,就是悬垂指针定时炸弹。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










