直接用std::atomic的compare_exchange_weak会出错,根本原因是ABA问题:线程A读到top==p后挂起,线程B弹出p、复用其地址压入新节点,A恢复后CAS误判“未变”而成功,导致链表逻辑错误或use-after-free;解决办法是将指针与版本号原子绑定(如__int128打包),并确保is_lock_free()为true。

为什么直接用 std::atomic 的 compare_exchange_weak 会出错?
多数人第一次写无锁栈时,会把 top 指针声明为 std::atomic<node></node>,然后在 push 中直接调用 compare_exchange_weak——结果是偶发崩溃或无限重试。根本原因在于 ABA 问题:线程 A 读到 top == p,被调度挂起;线程 B 把 p 弹出、又压入一个新节点 q,再弹出 q、再压入另一个 *地址相同但逻辑不同的* 节点 r(比如内存池复用);此时 A 恢复执行,compare_exchange_weak 仍认为“没变”,成功写回,却跳过了中间两次修改。
解决办法不是换函数,而是加版本号。主流做法是把指针和计数器打包成 128 位整数(如 __int128 或 std::atomic<uint64_t></uint64_t>),或者用 std::atomic<:pair size_t>></:pair>(需自定义特化)。但注意:x86-64 上 std::atomic<:pair size_t>></:pair> 通常不满足 lock-free,必须用 is_lock_free() 检查。
- GCC/Clang 下推荐用
__int128+std::atomic<__int128></__int128>(开启-m128bit或默认支持) - MSVC 不支持
__int128,得退回到 hazard pointer 或带引用计数的方案 - 别依赖
std::atomic<t>::load(memory_order_acquire)</t>返回值直接解引用——它可能刚被其他线程释放,要配合内存屏障或安全发布机制
push 和 pop 的 CAS 循环里,为什么不能只更新指针?
无锁栈的核心是“先读后写”原子操作,但读到的旧值必须包含足够信息来构造新状态。比如 push(x):你得把新节点 x 的 next 指向当前 top,再用 CAS 把 top 改成 x。如果只改指针、不设 x->next,多个线程并发 push 会导致链表断裂或节点丢失。
同理,pop 必须先读出 top 和 top->next,再用 CAS 把 top 改成 top->next。漏掉任一环节都会破坏结构一致性。
立即学习“C++免费学习笔记(深入)”;
-
push中new_node->next = old_top必须在 CAS 前完成,且不能被编译器重排(加std::atomic_thread_fence(std::memory_order_relaxed)不够,要用memory_order_acquireload +memory_order_releasestore 配合 - 所有节点分配建议走线程局部内存池(如
tbb::scalable_allocator),避免 malloc 竞争成为瓶颈 - 不要在
pop成功后立即delete节点——其他线程可能还在读该节点的next字段,需用 hazard pointer 或 epoch-based reclamation(EBR)延迟释放
如何验证你的 Lock-Free Stack 真的是 lock-free?
光看没用 mutex 不代表 lock-free。真正标准是:任意线程长时间阻塞或崩溃,不影响其他线程继续完成操作。验证不能只靠跑通,得测行为。
最有效方式是注入故障:用 gdb 在某个线程的 CAS 循环中打断点并暂停,观察其他线程是否能持续 push/pop 不卡死。再进一步,用 helgrind 或 ThreadSanitizer 检查是否存在隐式锁(比如 std::cout、静态变量初始化、malloc 内部锁)。
- 编译时加
-fsanitize=thread,运行时看是否报 data race —— 即使没 crash,有 race 也说明内存访问未正确同步 - 用
std::atomic_is_lock_free(&top)检查底层是否真的用 CPU 原语(如cmpxchg16b),而非模拟实现 - 压测时监控
perf stat -e instructions,cycles,cache-misses:lock-free 栈应显著减少 cache line bouncing,miss rate 比基于 mutex 的低 30%+ 才算合格
Windows 下 InterlockedCompareExchange128 怎么安全封装?
WinAPI 提供 InterlockedCompareExchange128,但它要求 16 字节对齐的缓冲区,且参数顺序反直觉:前两个参数是低位/高位指针,第三个是期望值低位,第四个是期望值高位,第五个是交换值低位,第六个是交换值高位。直接裸用极易传错参数顺序或对齐失败。
安全做法是定义结构体并强制对齐,再封装成类成员函数:
struct alignas(16) TaggedPtr {
Node* ptr;
size_t tag;
bool compare_exchange_weak(TaggedPtr& expected, TaggedPtr desired) {
// 调用 InterlockedCompareExchange128,注意参数顺序
// ……(具体实现略,关键在按文档传参)
}
};
- 别用
#pragma pack改变对齐——它会让alignas(16)失效 - MSVC 中
TaggedPtr实例必须分配在 16 字节边界上(new 通常满足,栈变量需alignas(16) TaggedPtr tp;) - Linux 下对应的是
__atomic_compare_exchange_nwith__int128,接口更统一,跨平台建议优先抽象出 CAS 接口层
实际写出来才发现,ABA 不是理论问题,而是每秒几万次 push/pop 时必现的崩溃;内存释放时机也不是“等没人用了再删”,而是得精确到每个指针的最后一次可见读。这些细节不亲手调一次 perf record -e 'syscalls:sys_enter_mmap' 看分配热点,不挂 gdb 看 CAS 失败率,根本意识不到。



















