std::atomic的原子性由cpu硬件指令实现,非编译器魔法或锁机制;其操作需配合恰当memory_order防止重排,多变量协同仍需mutex。

std::atomic 的原子性靠硬件指令实现,不是编译器魔法
它不靠锁,也不靠操作系统调度保证原子性,而是把 load、store、fetch_add 这类操作翻译成 CPU 提供的原子指令,比如 x86 上的 lock xadd、mov + mfence 组合,或 ARM 上的 ldxr/stxr 循环。编译器只负责生成正确指令序列,并插入必要的内存屏障(通过 memory_order 参数控制),不会擅自重排这些操作。
常见错误现象:std::atomic<int> x = 0;</int> 然后多个线程同时执行 x++,结果却小于预期——这是因为 x++ 展开为读-改-写三步,必须用 fetch_add(1) 才真正原子;直接赋值 x = 5 是原子的,但 x += 3 不是语法糖,它等价于 x.fetch_add(3),这点容易误判。
-
std::atomic对象必须满足 trivially copyable,且底层类型需支持对应平台的原子指令(例如std::atomic<:string></:string>非法) - 非 lock-free 类型(可通过
x.is_lock_free()检查)会退化为内部互斥锁,性能断崖下跌,嵌入式或实时场景务必确认返回true - 未指定
memory_order时默认用memory_order_seq_cst,安全但可能拖慢性能;高频更新计数器可考虑memory_order_relaxed,但要自己确保逻辑无依赖
memory_order 不是可选项,选错会导致读写乱序
它决定编译器和 CPU 能否对原子操作前后其他内存访问做重排。选错不会崩溃,但会让看似正确的多线程逻辑在优化后出错——比如生产者写完数据再置标志位,消费者看到标志位就去读数据,若标志位用 memory_order_relaxed,编译器可能把数据写入移到标志位之后,消费者就拿到脏数据。
使用场景举例:自旋锁中 flag 变量通常用 memory_order_acquire(加锁)和 memory_order_release(解锁),既避免重排又比 seq_cst 轻量;计数器累加若无依赖关系,fetch_add(1, std::memory_order_relaxed) 完全够用。
立即学习“C++免费学习笔记(深入)”;
-
memory_order_acquire保证其后所有读操作不被提前到该原子读之前 -
memory_order_release保证其前所有写操作不被推迟到该原子写之后 -
memory_order_acq_rel同时具备两者特性,适用于fetch_add、compare_exchange_weak等读-改-写操作 - 不要为了“看起来高级”而滥用
memory_order_consume,目前主流编译器基本无视它,行为等同于acquire
compare_exchange_weak 和 compare_exchange_strong 的区别不只是性能
它们都做“比较并交换”,但 weak 版本允许伪失败(spurious failure):即使当前值等于期待值,也可能返回 false。这在某些架构(如 ARM、LL/SC 实现)上是硬件限制,不是 bug。而 strong 版本承诺只要值匹配就成功,代价是可能触发更重的指令序列或循环重试。
典型错误:写成 if (x.compare_exchange_weak(expected, desired)) { ... },期望一次判断搞定——但 weak 可能失败,必须放在循环里重试;而 strong 虽然不用循环,但在高竞争下反而可能比 weak 更慢。
- 绝大多数无锁数据结构(栈、队列节点插入)都用
weak+ 循环,因为伪失败概率低,且循环开销远小于强版本的潜在代价 - 仅当逻辑上“只尝试一次”有意义时才用
strong,比如初始化单例指针,失败就放弃而不是重试 - 注意
expected参数是引用,调用后若失败会被更新为当前实际值,这是为了方便下次循环直接复用
std::atomic 不能替代 mutex,尤其涉及多个变量协同时
它只能保证单个对象的读写原子性。两个 std::atomic<int></int> 变量之间没有事务性,无法实现“一起更新”或“原子地读取二者”。试图用多个原子操作拼出临界区,极易掉进 ABA 问题或状态撕裂陷阱。
比如实现一个带版本号的指针容器,只用 std::atomic<t></t> 和 std::atomic<int></int> 分别存指针和版本,compare_exchange_weak 时无法同时验证二者——这就是经典的 ABA 场景,必须用 std::atomic<:pair int>></:pair>(需满足 trivially copyable)或更稳妥的 std::atomic<uint64_t></uint64_t> 手动打包。
- 跨多个原子变量的逻辑一致性,优先考虑
std::mutex,无锁只是特定场景的优化手段,不是银弹 -
std::atomic_flag是唯一保证 lock-free 的类型,适合实现自旋锁基元,但它只有test_and_set和clear,功能极简 - 调试困难:无锁代码出问题往往表现为偶发、难以复现的数据错乱,gdb 断点可能干扰时序,导致问题消失
真正难的从来不是写对一个 fetch_sub,而是理清所有共享状态的修改顺序、依赖关系和内存序边界。这些东西没法靠文档速查,得一行行看汇编、用 tsan 跑压力测试,或者干脆先用 mutex 写通再逐步替换。











