compare_exchange_strong在无锁栈中必须循环重试,因其失败仅表示预期值不匹配(如其他线程已修改栈顶),而非错误;不循环会导致丢数据,且失败时expected自动更新为当前值,需在循环内重新构建new_top->next以确保链接正确。

为什么 compare_exchange_strong 在无锁栈里必须用循环重试
因为 compare_exchange_strong 可能失败——不是出错,而是「预期值不匹配」。在多线程压栈时,另一个线程可能刚修改了栈顶指针,导致你读到的旧 top 已失效。不循环就直接返回,会丢数据。
典型错误写法是只调用一次就放弃:
Node* expected = top.load(); Node* new_top = new Node(data); new_top->next = expected; top.compare_exchange_strong(expected, new_top); // ❌ 失败就不管了
正确做法是用 while 循环直到成功:
- 每次循环都重新读取当前
top值作为expected -
compare_exchange_strong成功则退出;失败则自动更新expected为当前实际值(这是它的语义) - 注意:必须把
new_top->next = expected放在循环内,否则会用过期的expected链接
pop 操作中 ABA 问题怎么绕过
单纯靠 compare_exchange_strong 无法防止 ABA:线程1读到 top == A,被挂起;线程2把 A 弹出、又压入新节点 A'(地址碰巧相同),线程1恢复后仍认为 “还是原来的 A”,接着用旧 A->next 覆盖栈顶——结果跳过了中间所有操作。
立即学习“C++免费学习笔记(深入)”;
常见解法是给指针加版本号(tagged pointer):
- 用
std::atomic<uint64_t></uint64_t>存ptr + version,低 3 位存版本,高位存指针(假设指针 8 字节对齐,末 3 位恒为 0) - 每次 CAS 时同时比对指针和版本号;成功 pop 后版本号 +1
-
std::atomic<Node*>本身不带版本,所以不能直接用它做 CAS 目标
示例关键逻辑:
struct TaggedPtr {
Node* ptr;
uint64_t version;
};
// 用 union 或位运算打包进 uint64_t,CAS target 是 atomic<uint64_t>
compare_exchange_strong 的 memory_order 怎么选
无锁栈对内存序很敏感:太松散会导致其他线程看不到最新状态;太严格又拖慢性能。推荐组合是:
-
push:用memory_order_release写新节点的next,再用memory_order_acq_rel做 CAS —— 确保新节点内容已写完,且栈顶更新对其他线程可见 -
pop:CAS 用memory_order_acq_rel,读出节点后用memory_order_acquire读其next—— 防止指令重排把后续读next提前到 CAS 之前 - 绝对不要用
memory_order_relaxed做栈顶 CAS,它无法保证其他线程观察到修改顺序
析构节点时为什么不能直接 delete
因为无锁结构下,某个线程可能正处在 CAS 中间:它已读到要弹出的节点 A,但还没完成 CAS 更新 top,此时 A 被另一个线程 delete 掉,第一个线程就会访问野指针。
必须延迟回收,常见方案有:
- Hazard Pointer:每个线程声明自己正在使用的节点(hazard pointer),回收前检查所有线程是否还在引用该节点
- RCU(Read-Copy-Update):适合读多写少,但 C++ 标准库无原生支持,需第三方实现
- 简易引用计数 + 延迟链表:pop 出节点后不删,先挂到本线程局部链表,等下一次 push 时批量检查并安全释放(需配合 epoch 或屏障)
没做延迟回收的代码,压测几分钟大概率 crash,而且复现困难。


















