relaxed内存序仅保证原子操作本身不可分割,不约束指令重排,需配合acquire-release同步链才能建立happens-before关系,适用于计数器、无锁数据结构内部节点操作等对中间顺序无要求的场景。

relaxed 内存序只保证原子性,不保顺序
用 std::memory_order_relaxed 的时候,编译器和 CPU 都可以重排它前后的读写——只要不破坏单线程内的数据依赖。它只确保这个原子操作本身是不可分割的,别的啥也不管。
常见错误现象:以为两个 relaxed 写能靠前后顺序“通知”其他线程,结果另一线程看到一个更新了、另一个还是旧值,甚至全乱序。
- 适合计数器(如引用计数、性能统计),只要最终值对,中间态谁先谁后无所谓
- 适合 flag 类型的“信号”,但必须配合其他同步手段(比如另一个 acquire-release 对)才能传递语义
- 不能单独用于实现锁、生产者-消费者队列头尾指针、或任何需要“先写 A 再写 B,别人得按这顺序看到”的逻辑
relaxed 和 acquire/release 搭配才真正有用
单独用 relaxed 几乎没意义;它的价值在于和 acquire/release 组合,减少不必要的内存屏障开销。比如无锁队列里,只有 head/tail 的“协调点”需要 acquire/release,中间节点链接可以用 relaxed。
使用场景:自旋锁的内部状态轮询、RCU 风格的指针切换、某些 lock-free stack 的 pop 操作中对 next 指针的读取。
-
load(std::memory_order_relaxed)读指针后,必须用compare_exchange_weak带acquire才能安全解引用 - 写共享数据用
relaxed,但写完立刻用store(std::memory_order_release)更新一个哨兵变量,让其他线程通过load(std::memory_order_acquire)看到整个写入效果 - ARM/AArch64 上
relaxed几乎没额外开销;x86 上虽然大部分指令天然有强序,但编译器仍可能重排,所以不能省
别在 x86 上误以为 relaxed “很安全”
x86 的 TSO 模型让很多 relaxed 表现像 acquire/release,但这只是硬件巧合。一旦代码迁移到 ARM 或 RISC-V,或者编译器升级优化策略,relaxed 就会暴露真实行为——重排、延迟可见、甚至永久看不到更新。
典型坑:本地测试一切正常,CI 跑在 ARM 构建机上时偶发 crash,gdb 里发现某个 relaxed load 读到了完全过期的值,而你以为它“应该已经更新了”。
- 调试时加
std::atomic_thread_fence(std::memory_order_seq_cst)临时验证是否是重排问题,但别留着上线 - Clang/GCC 的
-fsanitize=thread能捕获部分 relaxed 使用不当,但不是所有逻辑错误都能报 - 如果不确定要不要更强的序,先用
acquire/release;等 profile 确认这里真是瓶颈,再逐个评估能否降级为relaxed
relaxed 不等于“随便用”,它要求你画清楚同步图
用 relaxed 的地方,必须能在纸上画出明确的 happens-before 边:哪次 store 通过哪个 release-acquire 链被哪个 load 观察到。没有这条链,relaxed 就只是个带原子语义的普通变量。
容易被忽略的是:同一个原子变量上混用不同内存序(比如一部分 load 用 relaxed,一部分用 acquire)是合法的,但语义由最弱那次操作决定——也就是说,哪怕你只在一个地方用了 relaxed load,整条同步路径就可能断掉。
- 不要靠“我只读不写”假设安全;读操作的序决定了你能看到什么
- std::shared_ptr 的引用计数内部用
relaxed,是因为它只关心数值正确性,不参与跨线程控制流 - 一旦涉及“等待某个条件成立”,哪怕只是 while 循环里
load(relaxed) == false,也得确认外面有配套的 release store,否则循环可能永远不退出
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










