结论:用标准c++实现正确、线程安全、无锁且支持任意并发插入/遍历的单向链表几乎不可能简单做到;难点在于内存重排、aba问题和内存回收,仅靠std::atomic无法解决。

为什么无锁单向链表在C++里很难真正“简单”
直接说结论:用标准C++(不依赖第三方库)实现一个**正确、线程安全、无锁、且支持任意并发插入/遍历**的单向链表,几乎不可能做到“简单”。std::atomic能帮你做原子操作,但无锁(lock-free)的核心难点不在原子读写,而在**内存重排、ABA问题、内存回收**——这些不是加个atomic_load就能绕过的。
常见误区是只用std::atomic<t></t>替换原始指针,以为赋值和CAS就万事大吉。结果是:程序偶尔崩溃、遍历时访问已释放节点、或插入丢失——而且复现困难。
- ABA问题:节点A被弹出→内存被复用为新节点A'→CAS误判为“仍是A”,导致逻辑错乱
- 内存回收:某个线程刚读到节点p,另一线程已delete它;你不能靠引用计数(那就不算纯无锁),也不能裸用
delete - 遍历与修改冲突:无锁结构通常不保证遍历一致性,遍历时可能跳过新插入节点,或撞上正在被卸载的节点
用std::atomic + compare_exchange_weak实现基础插入(仅push_front)
如果你只需要**单生产者单消费者(SPSC)或仅支持并发push_front**,且能接受“遍历不可靠”,可以写出可工作的最小原型。关键点是所有指针操作必须用std::atomic封装,并用CAS循环重试。
struct Node {
int data;
std::atomic<node> next{nullptr};
};
<p>class LockFreeStack {
std::atomic<node>> head{nullptr};
public:
void push(int val) {
Node node = new Node{val, nullptr};
Node* old_head = head.load();
do {
node->next.store(old_head, std::memory_order_relaxed);
} while (!head.compare_exchange_weak(old_head, node,
std::memory_order_acquire, std::memory_order_relaxed));
}
};</node></p></node>
注意:compare_exchange_weak必须配合do-while循环,因为可能因伪失败(spurious failure)返回false;memory_order_acquire确保后续读操作不会重排到CAS之前;memory_order_relaxed用于内部next赋值——这里不需要同步语义。
- 不要用
new裸分配:实际项目中应搭配对象池或RCU式回收,否则delete时机无法控制 - 这个版本**不支持pop或遍历**:一旦加入pop,就必须处理ABA和内存回收,复杂度指数上升
- 不能用
std::shared_ptr替代:原子智能指针(std::atomic_shared_ptr)直到C++20才部分支持,且引用计数本身有锁
ABA问题怎么绕不过?必须用Hazard Pointer或RCU
只要涉及节点复用(比如从空闲池取旧节点再用),ABA就是硬伤。标准库没提供解决方案,你得自己实现轻量级防护机制。最可行的是Hazard Pointer(危险指针),比RCU更轻,适合小规模场景。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
核心思想:每个线程声明自己正在使用的节点(hazard pointer),其他线程删除节点前,必须确认没有线程将其标记为hazard。这需要额外的全局hazard数组和周期性扫描。
- 别手写Hazard Pointer全实现——容易漏掉
memory_order_seq_cst屏障或扫描竞态;推荐用libcds或folly::AtomicUnorderedLinkedList - C++20的
std::atomic<t>::wait</t>/notify对无锁链表帮助极小,它解决的是等待通知,不是内存生命周期管理 - 如果业务允许“延迟回收”(如每10ms批量清理一次),可以用epoch-based reclamation(EBR),但需协调所有线程的epoch推进
什么时候该放弃无锁,改用std::mutex?
除非你的链表每秒被操作百万次以上,且性能剖析明确显示锁是瓶颈,否则用std::mutex保护一个普通链表更可靠、更易维护。
典型信号是:你开始为每个节点加std::atomic_flag尝试手工标记、或翻阅Herb Sutter论文找内存序组合、或发现测试要跑一小时才偶现crash——这时候,锁不是退步,是止损。
-
std::mutex保护的链表,在现代glibc/LLVM上争用开销远低于预期;很多所谓“高并发”场景其实QPS不到5k - 若真需要吞吐量,优先考虑分片(sharding):比如按哈希把链表拆成8个,各自加锁,比单个无锁结构更容易验证正确性
- 无锁代码的调试成本远高于逻辑复杂度——core dump里看不到“谁删了这个节点”,只能靠日志和内存断点硬啃
真正难的从来不是写CAS,而是证明它在所有执行路径下都不破坏不变量。这点没人能跳过。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










