原子指针的compare_exchange_strong会误判aba,因其仅比较指针值而忽略中间状态变化;需用版本号(如高位存版本、低位存对齐指针)或shared_ptr等间接规避,且须妥善处理版本号溢出。

为什么原子指针的 compare_exchange_strong 会误判 ABA
当一个指针被反复修改为同一地址(比如先从 A → B → A),而中间有其他线程完成了完整的一次“弹出再压入”,compare_exchange_strong 仅比对指针值,会认为状态没变、操作成功,实际已丢失中间更新。这不是函数 bug,而是设计使然——它只保证指针值的原子性,不带逻辑语义。
用 std::atomic<uint64_t></uint64_t> 手动拼装版本号+指针
标准库没有直接提供带版本号的原子指针,但可以用 std::atomic<uint64_t></uint64_t> 存储高位为版本号、低位为指针的组合值。常见做法是:假设指针 8 字节对齐(最低 3 位恒为 0),则用低 3 位空闲位存指针,高 61 位存版本号。
实操要点:
- 用
reinterpret_cast<uintptr_t>(ptr)</uintptr_t>转指针为整数,再左移 3 位腾出低位 - 版本号每次 CAS 成功后自增,避免用
fetch_add(1)直接加——必须和指针更新绑定在同一次 CAS 中 - CAS 比较时,要同时校验组合值的高低位;交换时,也必须一次性写入新指针+新版本号
- 读取指针时,右移 3 位再
reinterpret_cast回指针类型;读版本号时,用掩码0x7FFFFFFFFFFFFFFF提取高位
示例关键片段:
uint64_t combined = (static_cast<uint64_t>(version) (ptr) >> 3); // ... 在 CAS 中使用 combined uintptr_t raw_ptr = (combined & ~0x7ULL) (raw_ptr);</uint64_t>
更稳妥的选择:std::atomic<:shared_ptr>></:shared_ptr> 或第三方库
std::shared_ptr 的控制块天然携带引用计数,配合 std::atomic 使用时,其 compare_exchange_strong 会比较控制块地址+引用计数,间接规避 ABA。但它有堆分配开销,且不能用于裸内存池管理场景。
若需高性能无锁结构(如自定义栈/队列),推荐用成熟实现:
-
absl::InlinedVector不适用,但absl::synchronization提供了AtomicPtr的封装思路 - Linux kernel 风格的
struct rcu_head+ 宽限期回收,适合读多写少 - Boost.Lockfree 的
stack<t></t>内部已用 tag-bit 方式处理 ABA
版本号溢出不是幻想,得真处理
61 位版本号看似够用,但在高频短生命周期节点(如每微秒压弹一次)下,约 2^61 纳秒 ≈ 70 年才溢出——听起来安全。但问题在于:溢出后版本号回绕,可能再次撞上旧值。
所以不能只靠“大概率不溢出”:
- 版本号字段必须是有符号整数(如
int64_t),用fetch_add并检查溢出,或改用循环计数器 + 全局 epoch 标记 - 更现实的做法是:在关键路径中记录最大版本号,一旦接近上限(如 > 2^60),触发一次轻量级全局重编号(rehash)或暂停写入做清理
- 某些场景下,干脆放弃纯无锁,改用带等待的乐观锁(如
std::mutex+ 双重检查),比版本号维护更简单可靠
真正难的从来不是拼出那个 uint64_t,而是想清楚:你的节点生命周期、更新频率、内存模型约束,是否真的值得为 ABA 多套一层版本逻辑。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











