relaxed内存序仅保证原子性,不保证操作顺序,适用于计数器递增、性能统计等无需同步的场景,但不可用于控制流判断或资源释放前的可见性同步。

relaxed 内存序只保证原子性,不保顺序
它只确保 load 或 store 本身是原子的,编译器和 CPU 都可以重排它前后的内存访问。这意味着:你不能靠它同步两个线程对不同变量的操作,也不能用它实现锁、信号量、引用计数递减后释放资源这类依赖顺序的逻辑。
常见错误现象:std::memory_order_relaxed 用于标志位但没配对 acquire/release,结果另一个线程看到标志为 true,却读到未初始化的数据 —— 这不是罕见 bug,是必然发生。
- 适用场景:计数器(如性能统计)、引用计数的递增(
fetch_add(1, std::memory_order_relaxed)) - 不适用场景:控制流判断(如 while(!done) {…} 中的
done.load(std::memory_order_relaxed))、资源释放前的可见性同步 - 注意:x86 上 relaxed 和 acquire/release 的指令码可能一样,但语义不同;ARM/AArch64 则会生成明显不同的 barrier 指令
计数器类场景下 relaxed 是安全且高效的
比如埋点上报中的事件计数、对象构造/析构次数统计、缓存命中率累加 —— 这些只要求“数值最终一致”,不要求某次 increment 一定在某次 load 之前被看到。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 用
std::atomic<int>::fetch_add()</int>或operator++配std::memory_order_relaxed,避免无谓的 barrier 开销 - 别把它和非原子变量混用:比如
counter.fetch_add(1, std::memory_order_relaxed)后直接读普通int变量,顺序仍不确定 - 如果后续要基于该计数做决策(比如“超 1000 次就 flush”),那判断动作本身必须用更强序(至少
acquire)
引用计数递增可用 relaxed,递减不行
shared_ptr 内部对引用计数的 fetch_add(1, std::memory_order_relaxed) 是标准做法,因为增加引用不会导致资源释放,不需要同步其他内存。
但递减不同:当计数归零时,必须确保此前所有对该对象的写操作都已完成,并对销毁线程可见。所以 fetch_sub(1, std::memory_order_acq_rel) 是必须的。
- 错误写法:
cnt.fetch_sub(1, std::memory_order_relaxed)+ if (cnt == 0) delete ptr —— 释放时可能看到脏数据 - 正确组合:递减用
acq_rel,delete 前的读取用acquire(或直接依赖acq_rel的同步效果) - 注意:即使只在一个线程里递减,只要存在多线程共享该计数,就必须按规则来
relaxed 不等于“随便用”,它要求程序员自己建模同步契约
它把内存顺序责任完全交给开发者:你得明确知道哪些变量之间需要什么级别的可见性,哪些操作之间必须有 happens-before 关系。没有自动兜底,也没有运行时检查。
容易被忽略的一点是:relaxed 操作在同一个线程内虽然不阻止重排,但它仍参与该线程的修改顺序(modification order),这点常被误认为“完全无序”。也就是说,同一原子变量上的多个 relaxed 操作,在该变量视角下仍有全局一致的顺序 —— 但这不帮你同步其他变量。
复杂点在于:你得同时考虑编译器优化(如 -O2 下的 load 提升)和 CPU 乱序执行(如 ARM 的弱一致性模型),而 relaxed 对两者都不设防。一旦漏掉一个 acquire/release 配对,bug 往往只在特定平台、特定负载下暴露。