应优先选用标准原子整型别名(如std::atomic_int),避免对自定义结构体直接套用std::atomic;memory_order按需选relaxed/release/acquire而非全用seq_cst;循环CAS必须用compare_exchange_weak;ABA问题需用tagged pointer等方案规避。

std::atomic 该选什么类型?别直接套用 int
原子变量不是万能胶水,std::atomic<int></int> 能用,但多数真实场景下它掩盖了对齐和大小的隐患。比如在结构体里嵌套 std::atomic<bool></bool>,某些平台可能因内存布局导致非原子读写;又比如用 std::atomic<long></long> 在 32 位 Windows 上,实际是锁模拟(InterlockedExchange64),性能反不如手写自旋。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 优先用标准原子整型别名:
std::atomic_int、std::atomic_uintptr_t,它们保证底层操作是 lock-free 的(可通过.is_lock_free()检查) - 避免对自定义结构体直接套
std::atomic<mystruct></mystruct>—— 即使编译通过,大概率 fallback 到内部互斥锁,失去无锁意义 - 若需原子更新多个字段(如 x/y 坐标),不要拼
std::atomic<uint64_t></uint64_t>手动位拆分,改用std::atomic<:pair int>></:pair>并确认is_lock_free()返回 true
memory_order 怎么选?别全用 memory_order_seq_cst
memory_order_seq_cst 是默认且最安全的顺序,但它强制全局顺序,在多核 CPU 上会插入大量内存屏障(如 mfence),吞吐量可能比 relaxed 模式低 3–5 倍。而多数计数器、状态标志根本不需要这么强的约束。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 计数器累加(如引用计数):用
memory_order_relaxed,只要不依赖其他内存访问顺序 - 生产者-消费者信号(如 flag 变成 true 表示数据就绪):写端用
memory_order_release,读端用memory_order_acquire,确保 flag 之前的写操作对读端可见 - 绝对避免在循环中反复调用
load(memory_order_seq_cst)等待条件 —— 它会阻止编译器优化,且每次都刷全核缓存一致性协议
compare_exchange_weak 和 strong 什么时候换?
compare_exchange_weak 可能在无竞争时也失败(spurious failure),尤其在 ARM 或旧版 x86 上;compare_exchange_strong 保证只在值不匹配时失败。但 weak 版本生成的汇编更轻(比如 ARM 的 ldxr/stxr 对),循环重试开销更低。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 所有循环 CAS 场景(如无锁栈 push/pop)必须用
compare_exchange_weak,配合 do-while,这是标准写法 - 单次尝试、不能重试的逻辑(如初始化全局原子指针)才考虑
compare_exchange_strong - 别在 weak 失败后直接 return 或 log —— 它失败不代表冲突,大概率是硬件抖动,应继续循环
无锁 ≠ 无风险:ABA 问题怎么绕过去?
典型现象:线程 A 读到指针 p = 0x1000,被抢占;线程 B 把 p 指向的对象释放,又新分配到同一地址 0x1000 并修改内容;A 恢复后执行 compare_exchange 成功,却误以为对象没变 —— 数据被静默覆盖。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 简单计数器场景(如 tag + ptr 组合):用
std::atomic<:shared_ptr>></:shared_ptr>,其内部已做 tag 处理(部分实现) - 手动解决:把指针和版本号打包进
uint64_t,高位存版本(每次释放+1),低位存指针,用compare_exchange_weak原子更新整个 64 位值 - 别指望
std::atomic_flag能躲开 ABA —— 它连 load 都不支持,纯用于自旋锁基元,和业务逻辑无关
真正难的从来不是写对一个 fetch_add,而是厘清哪些变量之间存在隐含依赖、哪些顺序约束被你无意中删掉了。稍不注意,lock-free 就退化成 lock-heavy。











