单纯用两个独立的std::atomic变量(指针和版本号)无法防止aba问题,因为读取二者非原子,存在竞态窗口;正确做法是将指针与版本号打包为单个无锁原子结构体(如taggedptr),并配合hazard pointer等安全内存回收机制。

单纯给指针加一个 std::atomic<uint64_t></uint64_t> 版本号,根本防不住 ABA 问题——因为读指针和读版本号不是原子的,中间有竞态窗口。
为什么分开维护指针和版本号会失效
常见错误是定义两个独立的原子变量:std::atomic<node></node> 和 std::atomic<uint64_t></uint64_t>。线程 A 在 load() 指针后、load() 版本号前,可能被调度暂停;此时线程 B 已完成“弹出 → 释放 → 复用地址 → 入栈”,指针值回退到原值,版本号却已 +2。线程 A 接着读到旧版本号,误判为“未变”,compare_exchange_weak 成功,use-after-free 就发生了。
关键点在于:compare_exchange_weak 只能保证单个 std::atomic 的读-改-写原子性,无法跨变量同步。
- 不能靠两次
load()拼凑“一致快照” - 也不能靠顺序调用
load()+compare_exchange_weak()来模拟——中间状态不可控 - 即使加内存序(如
memory_order_acquire),也无法消除两个变量间的时间差
必须用单个 lock-free 的复合结构体
正确做法是把指针和版本号打包进一个结构体,再用 std::atomic 包裹它,并确保该类型支持无锁原子操作。否则 fallback 到内部互斥锁,就不是无锁了,还可能死锁。
示例结构体(x86-64 安全):
struct TaggedPtr {
uintptr_t ptr : 48;
uint64_t version : 16;
};
注意三点:
- 先检查
std::atomic<taggedptr>::is_lock_free()</taggedptr>,返回false就别硬上 - 位域顺序很重要:指针放低位、版本放高位,适配
cmpxchg16b对齐习惯 - 版本字段至少用
uint64_t,避免回绕(uint32_t在高并发下几秒就溢出)
std::atomic<taggedptr></taggedptr> 的典型使用陷阱
很多人写完结构体就直接用,结果在 GCC 11 或旧版 Clang 上 silent fallback 到锁实现——因为 16 字节结构体在某些 ABI 下不满足无锁条件。这不是编译错误,而是运行时性能断崖。
实际项目中,TaggedPtr 很难彻底规避内存回收复杂性。比如:pop 出的节点要等所有线程都不再引用它,才能安全 delete。这时 hazard pointer 是更落地的选择。
它不防 ABA,但防 use-after-free:每个线程声明自己正在访问哪些指针(hazard pointers),回收线程只清理那些未被任何 hazard pointer 标记的节点。ABA 依然可能发生,但“B 变回 A”时,旧 A 的内存不会被重用 → 地址复用链断开。
真正容易被忽略的是:就算你把 TaggedPtr 写对了、is_lock_free() 返回 true 了,只要没配套做安全内存回收(比如 hazard pointer 或 RCU),ABA 风险只是被掩盖,不是被解决。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











