head.store(new_node) 不行,因为它跳过一致性校验,导致多线程下丢失更新;必须用 compare_exchange_weak 循环CAS,确保“读-改-写”原子性,并在失败时重试刷新视图,同时正确处理内存序与节点回收。

无锁栈在 C++ 多线程中能避免 std::mutex 带来的阻塞和上下文切换开销,但**不能直接靠 std::atomic 赋值就完事**——必须用 CAS 循环 + 正确内存序,否则会丢数据、崩溃或触发 ABA 问题。
为什么 head.store(new_node) 不行
单纯把新节点指针写入原子头指针,跳过了“读-改-写”一致性校验。多线程下,两个线程同时读到旧 head,各自创建新节点并直接覆盖 head,后写的会覆盖先写的,导致一个节点永久丢失(即“丢失更新”)。这本质上是竞态条件,不是原子性不够,而是逻辑没闭环。
-
head.load()只读一次,无法保证后续操作时该值仍有效 - 必须用
compare_exchange_weak或compare_exchange_strong原子地验证“我读到的旧值是否仍是当前值”,再决定是否更新 - 失败时要重试,而不是继续往下走
push 的标准 CAS 循环怎么写
核心是三步:读当前头 → 构造新节点 → CAS 替换头。失败就刷新本地视图,再试。注意 next 指针赋值必须在 CAS 之前,且每次失败后都要更新,否则新节点会指向过期地址。
template <typename T>
void push(const T& value) {
node* new_node = new node(value);
node* old_head;
do {
old_head = head.load(); // 读当前头
new_node->next = old_head; // 指向它
} while (!head.compare_exchange_weak(old_head, new_node)); // 成功则退出,失败则 old_head 被更新为最新值
}- 用
compare_exchange_weak更高效(x86 上通常编译为单条CMPXCHG),但需配合 do-while 循环处理“伪失败” - 不能把
new_node->next = old_head放在循环外,否则失败重试时仍用第一次读到的过期old_head - 不显式指定内存序时,默认是
std::memory_order_seq_cst,安全但略重;高吞吐场景可降为memory_order_release(写)+memory_order_acquire(读),但需同步分析整个结构
pop 为什么比 push 更危险
pop 不仅要 CAS 更新头指针,还要安全释放被弹出的节点内存——而其他线程可能正拿着它的指针做 next 遍历。裸 delete 会导致 use-after-free。
立即学习“C++免费学习笔记(深入)”;
- 最简实现(仅演示逻辑)会先 CAS 获取旧头,再 CAS 更新头为
old_head->next,但中间若被其他线程修改了old_head->next,就会出错 - 真实项目必须引入内存回收机制,比如
hazard pointer或RCU,否则无法安全 delete - 即使只读不删,
pop的 CAS 循环也比push多一层嵌套:要确保old_head非空,且old_head->next在两次读之间未被改写(ABA 风险)
真正落地时,pop 的正确性远超 push;很多所谓“无锁栈”示例只实现了 push,就是卡在这一步。别忽略内存回收——它不是锦上添花,而是必选项。


















