编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
      • C++内存模型基石
      • 六大内存序详解
        • 1. 案例引入
          • 1.1 双重检查锁的 relaxed 失败
          • 1.2 无锁队列的 acq_rel 混淆
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 六种内存序的三级分类
          • 2.2 为何这么切
        • 3. relaxed —— 仅保证原子性
          • 3.1 安全的 relaxed 使用场景
          • 3.2 经典反例:基于 relaxed 的互斥
          • 3.3 relaxed 的真实汇编开销
        • 4. acquire / release —— 单向屏障
          • 4.1 acquire 的语义与可移动边界
          • 4.2 release 的语义与可移动边界
          • 4.3 acquire-release 配对的完整证明
          • 4.4 消息传递范式——经典 acquire-release 用例
        • 5. acq_rel 与 RMW 操作
          • 5.1 fetchadd 的两种写法——relaxed 与 acqrel 的区别
          • 5.2 CAS 循环中的内存序选择
          • 5.3 无锁栈的完整 acquire-release 推理
        • 6. seq_cst —— 全局统一时间线
          • 6.1 单个全局序的语义
          • 6.2 seq_cst 的代价——x86 mfence 和 ARM dmb
          • 6.3 什么时候必须用 seq_cst
        • 7. consume —— 已废弃的优化
          • 7.1 consume 的设计初衷
          • 7.2 为什么编译器只能降级为 acquire
        • 8. happens-before 与 synchronizes-with 的证明系统
          • 8.1 四类基础关系
          • 8.2 happens-before 的传递闭包
          • 8.3 用 happens-before 证明并发正确性——完整案例
        • 9. 所有序的汇编全景与性能对比
          • 9.1 x86 vs ARM 的指令生成对比表
          • 9.2 benchmark——各内存序的代价量化
          • 9.3 x86 上为什么 seq_cst load 等于 acquire load
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 内存序选择决策树
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-06
目录

六大内存序详解

# 41.六大内存序详解

# 目录介绍

  • 1. 案例引入
    • 1.1 双重检查锁的 relaxed 失败
    • 1.2 无锁队列的 acq_rel 混淆
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 六种内存序的三级分类
    • 2.2 为何这么切
  • 3. relaxed —— 仅保证原子性
    • 3.1 安全的 relaxed 使用场景
    • 3.2 经典反例:基于 relaxed 的互斥
    • 3.3 relaxed 的真实汇编开销
  • 4. acquire / release —— 单向屏障
    • 4.1 acquire 的语义与可移动边界
    • 4.2 release 的语义与可移动边界
    • 4.3 acquire-release 配对的完整证明
    • 4.4 消息传递范式——经典 acquire-release 用例
  • 5. acq_rel 与 RMW 操作
    • 5.1 fetch_add 的两种写法——relaxed 与 acq_rel 的区别
    • 5.2 CAS 循环中的内存序选择
    • 5.3 无锁栈的完整 acquire-release 推理
  • 6. seq_cst —— 全局统一时间线
    • 6.1 单个全局序的语义
    • 6.2 seq_cst 的代价——x86 mfence 和 ARM dmb
    • 6.3 什么时候必须用 seq_cst
  • 7. consume —— 已废弃的优化
    • 7.1 consume 的设计初衷
    • 7.2 为什么编译器只能降级为 acquire
  • 8. happens-before 与 synchronizes-with 的证明系统
    • 8.1 四类基础关系
    • 8.2 happens-before 的传递闭包
    • 8.3 用 happens-before 证明并发正确性
  • 9. 所有序的汇编全景与性能对比
    • 9.1 x86 vs ARM 的指令生成对比表
    • 9.2 benchmark——各内存序的代价量化
    • 9.3 x86 上为什么 seq_cst load 等于 acquire load
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 内存序选择决策树
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 双重检查锁的 relaxed 失败

某项目用 relaxed 实现 DCL(双重检查锁)。在 x86 上跑了半年无 bug,部署到 ARM 服务器——偶发 SIGSEGV:

// ====== 事故代码 V1:relaxed DCL ======
std::atomic<Config*> config = nullptr;

Config* get_config() {
    auto* p = config.load(std::memory_order_relaxed);  // ① relaxed 读
    if (p != nullptr) return p;                         // ② 快速路径

    std::lock_guard<std::mutex> lock(g_mtx);
    p = config.load(std::memory_order_relaxed);         // ③ 再次 relaxed 读
    if (p == nullptr) {
        auto* new_cfg = new Config();                   // ④ 构造 Config
        config.store(new_cfg, std::memory_order_relaxed); // ⑤ relaxed 写
        p = new_cfg;
    }
    return p;
}

// 线程 0 在写 Config 的成员...
// 线程 1 通过 ① 读到了指针——但读到的 Config 对象还没有完全构造!
// 根因:relaxed 没有顺序保证——other thread 可能看到 config 指针,但看不到 Config 的成员初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

根因:config.store(ptr, relaxed) 只保证 ptr 的写入是原子的——不保证 new Config() 的构造函数在 store 之前对其他线程可见。 ARM 上的 store buffer 和 invalidate queue 可以让其他核先看到指针、后看到成员初始化。

# 1.2 无锁队列的 acq_rel 混淆

某无锁 SPSC 队列的作者在所有 fetch_add 和 load 上都用了 acq_rel——导致不必要的 fence 开销:

// ====== 事故代码 V2:滥用 acq_rel ======
struct SPSCQueue {
    std::atomic<int> write_pos{0};
    std::atomic<int> read_pos{0};

    bool try_push(int val) {
        int w = write_pos.load(std::memory_order_acquire);  // ⚠️ acquire 就够了
        int r = read_pos.load(std::memory_order_acquire);   // ✅ 需要 acquire
        // ...
        write_pos.store(w + 1, std::memory_order_release);  // ✅ 需要 release
    }
};

// write_pos.load(acquire) 在这里没必要——
//   只需要在 read_pos.load 后看到最新的消费者状态
//   每个 load 都加 acquire = 不必要的 dmb(ARM 上每个 ~20 cycles)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 1.3 七个待解疑问

① 六种内存序的语义分别是什么? 谁比谁更强?                            → 第 2-7 章
② 为什么 relaxed 只能用于计数器——不能用于互斥?                        → 第 3 章
③ acquire-release 配对是什么意思? 怎么证明 happens-before?              → 第 4 / 第 8 章
④ acq_rel 和 seq_cst 有什么区别? RMW 操作为什么需要 acq_rel?            → 第 5 / 第 6 章
⑤ consume 为什么被废弃? 和 acquire 有什么不同?                          → 第 7 章
⑥ 每种内存序在 x86 和 ARM 上生成什么指令? 性能差多少?                    → 第 9 章
⑦ 实战中怎么选择正确且最高效的内存序?                                    → 第 10 章
1
2
3
4
5
6
7

# 2. 架构概览

# 2.1 六种内存序的三级分类

┌─────────────────────────────────────────────────────────────┐
│                     C++ memory_order                          │
├───────────────┬─────────────────┬─────────────────────────────┤
│ ① relaxed    │ ② acquire       │ ⑥ seq_cst                   │
│   仅原子性    │   ③ release      │   全局统一顺序               │
│   无顺序保证  │   ④ acq_rel      │   所有线程看到同一时间线    │
│              │   双向屏障        │   最强——也最贵              │
│              │   ⑤ consume       │                             │
│              │   已废弃           │                             │
└───────────────┴─────────────────┴─────────────────────────────┘

强度递增 →  relaxed &lt; acquire/release &lt; acq_rel &lt; seq_cst
1
2
3
4
5
6
7
8
9
10
11
12

# 2.2 为何这么切

疑惑:为什么需要 6 种内存序——1 种「强」和 1 种「弱」不够吗?

论证:

  1. relaxed 的存在是因为「有些操作真的不需要 order」——递增原子计数器只需要原子性——不需要任何 happens-before 保证。加点序就多一次 fence——不必要。
  2. acquire/release 是硬件最小化的跨线程同步——只控制「单向屏障」。acquire 挡后面的、release 挡前面的。比 seq_cst 的全屏障便宜得多(ARM 上 dmb ishld vs dmb ish 差 30%)。
  3. acq_rel 是 RMW 的自然需求——CAS/fetch_add 同时是读和写——需要同时防前面的(acquire)和后面的(release)。
  4. seq_cst 需要全局总序——多核间通信最昂贵。仅在需要「所有线程看到同一条时间线」时用。

结论:6 种内存序是「成本 vs 正确性」的精确梯度——选择越弱的内存序,CPU 生成越少的 fence 指令。正确性的关键是选对、选刚好的内存序——不多付任何不必要的 fence。


# 3. relaxed —— 仅保证原子性

# 3.1 安全的 relaxed 使用场景

// ✅ 安全:全局计数器——不需要顺序保证
std::atomic<int> request_count{0};
void handle_request() {
    request_count.fetch_add(1, std::memory_order_relaxed);
    // 不需要任何 happens-before——只关心计数的最终值
}

// ✅ 安全:引用计数(shared_ptr 内部用 relaxed 递减 weak_count)
// 只要求「计数本身准确」——不建立任何跨线程先序
1
2
3
4
5
6
7
8
9

# 3.2 经典反例:基于 relaxed 的互斥

// ❌ 不安全——relaxed 不能用于互斥
std::atomic<bool> locked{false};

void lock() {
    while (locked.exchange(true, std::memory_order_relaxed)) {}
    // ❌ relaxed 不保证临界区代码和 locked 标志之间的顺序
    // → 临界区的内存操作可能被重排到 locked=true 之前或之后
}

void unlock() {
    locked.store(false, std::memory_order_relaxed);
    // ❌ 临界区的写可能被推迟到 unlock 之后才对其他核可见
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.3 relaxed 的真实汇编开销

操作 x86 ARM 延迟
load(relaxed) mov ldr ~1 ns
store(relaxed) mov str ~1 ns
fetch_add(relaxed) lock xadd ldadd ~15 ns

relaxed 是唯一在 x86 和 ARM 上都不生成任何 fence 指令的内存序——零开销。


# 4. acquire / release —— 单向屏障

# 4.1 acquire 的语义与可移动边界

acquire load = 「这行之后的读和写,不能被重排到这行之前」:

acquire load 之后的所有内存操作,不能越界到 acquire 之前

        acquire load A
        ───────────── barrier ─────────────
        可以移动到这里:
          load B
          store C
1
2
3
4
5
6
7

# 4.2 release 的语义与可移动边界

release store = 「这行之前的读和写,不能被重排到这行之后」:

        store A
        load B
        ───────────── barrier ─────────────
        release store C
        不能移动到这里
1
2
3
4
5

# 4.3 acquire-release 配对的完整证明

// 线程 0                              线程 1
data = 42                             while (!ready.load(acquire));
ready.store(true, release);           assert(data == 42);

// 证明 data==42 必然成立:
// ① data=42 (SB) ready.store(release)  ← 同一线程 sequenced-before
// ② ready.store(release) (SW) ready.load(acquire) ← 看到 true → synchronizes-with
// ③ ready.load(acquire) (SB) assert(read data)  ← 同一线程
// ①+②+③ → data=42 happens-before assert → data 一定 == 42
1
2
3
4
5
6
7
8
9

# 4.4 消息传递范式——经典 acquire-release 用例

// 生产者                             消费者
void send(Message msg) {            Message recv() {
    buf[idx] = msg;                     while (!ready[idx].load(acquire));
    ready[idx].store(true, release);    return buf[idx];
}                                   }
// release 保证 buf[idx]=msg 在 ready[idx]=true 之前对消费者可见
// acquire 保证 ready 被看到时才读 buf——不会看到未完成的 msg
1
2
3
4
5
6
7

# 5. acq_rel 与 RMW 操作

# 5.1 fetch_add 的两种写法——relaxed 与 acq_rel 的区别

// relaxed —— 只需要计数
tail.fetch_add(1, relaxed);  // 只保证原子递增——不需要建立任何顺序

// acq_rel —— 需要读旧值+写新值,且需要传消息
auto old = head.fetch_add(1, acq_rel);  // 同时有 acquire 和 release
// → 读旧值(acquire)保证后面看到的操作不会从这个 fetch_add 之前重排过来
// → 写新值(release)保证前面的操作不会重排到后面
1
2
3
4
5
6
7

# 5.2 CAS 循环中的内存序选择

// 无锁 push:CAS 用 acq_rel——需要读写双向屏障
node->next = head.load(relaxed);               // 只需要原子性
while (!head.compare_exchange_weak(node->next, node,
         std::memory_order_acq_rel,            // 成功时——acquire+release
         std::memory_order_relaxed)) {}        // 失败时——relaxed即可
1
2
3
4
5

# 5.3 无锁栈的完整 acquire-release 推理

// push_node 后→ head.store(new_node, release)
// → 保证 new_node 的初始化在 head 更新前对其他核可见

// pop→ head.load(acquire)
// → 保证后续对 node 内容的读取不会越过这个 acquire
// → 读到旧的 node 指针不会导致访问已释放的内存
1
2
3
4
5
6

# 6. seq_cst —— 全局统一时间线

# 6.1 单个全局序的语义

// 四个线程,所有 seq_cst 操作在所有线程看到的是同一个全局顺序
std::atomic<int> x{0}, y{0};

// 线程 0: x.store(1, seq_cst)       线程 2: y.store(1, seq_cst)
// 线程 1: r1=x.load(seq_cst)        线程 3: r2=y.load(seq_cst)
//                r2=y.load(seq_cst)           r1=x.load(seq_cst)

// 不可能出现 r1==1 && r2==1 && 线程1r2==0 && 线程3r1==0
// seq_cst 保证全局总序——所有线程「同意」x和y的可见顺序
1
2
3
4
5
6
7
8
9

# 6.2 seq_cst 的代价——x86 mfence 和 ARM dmb

操作 x86 ARM
load(seq_cst) mov (x86 Load 自带 acquire) ldr; dmb ishld
store(seq_cst) xchg 或 mov+mfence dmb ish; str; dmb ish
RMW lock xadd (自带全屏障) ldaddal (全屏障版本)

x86 seq_cst store 比 release store 多一条 mfence——~30 ns。

# 6.3 什么时候必须用 seq_cst

  • 多个 atomic 变量之间有顺序依赖,且需要全局统一序
  • 互斥锁的内部实现——std::mutex 依赖 seq_cst
  • 不确定时可以用 seq_cst 保证正确性——之后降级为 weaker order profiling

# 7. consume —— 已废弃的优化

# 7.1 consume 的设计初衷

memory_order_consume 的设计目的:比 acquire 更轻——只阻止依赖链的重排。

// consume 的理想行为(从未被编译器真正实现)
int* p = ptr.load(consume);  // 只保证 *p 的依赖链不被重排
int x = *p;                  // 这行不重排——因为 x 依赖 p
int y = global_y;            // 这行可能被重排——因为 global_y 不依赖 p
1
2
3
4

# 7.2 为什么编译器只能降级为 acquire

consume 要求编译器追踪「数据依赖链」——这在复杂 C++ 代码中几乎不可能。如:

int x = *p;
int y = *(p + x);  // y 依赖 p 也依赖 x——链式依赖
1
2

编译器为安全起见——所有主流编译器(GCC/Clang/MSVC)都把 consume 降级为 acquire。 标准在 C++17 中标记为「不推荐使用」——建议直接写 acquire。


# 8. happens-before 与 synchronizes-with 的证明系统

# 8.1 四类基础关系

① Sequenced-before (SB):同一线程内,A 写在代码里 B 之前
    例:data=42 (SB) ready.store(true)

② Synchronizes-with (SW):一个 release store 被一个 acquire load 「看到」
    例:ready.store(true, release) (SW) ready.load(acquire) [读到 true]

③ Happens-before (HB):A (HB) B = 存在从 A 到 B 的链
    链的每步是 SB 或 SW——传递闭包

④ Inter-thread happens-before:跨线程的 HB
    例:线程0中的A (HB) 线程1中的B
1
2
3
4
5
6
7
8
9
10
11

# 8.2 happens-before 的传递闭包

如果 A (HB) B 且 B (HB) C → A (HB) C

例:线程 0: data=42 → ready.store(true, release)
    线程 1: ready.load(acquire)[真] → use(data)

    data=42 (SB) ready.store
    ready.store (SW) ready.load (读到 true)
    ready.load (SB) use(data)

    → data=42 (HB) use(data) → data 一定 == 42 ✅
1
2
3
4
5
6
7
8
9
10

# 8.3 用 happens-before 证明并发正确性——完整案例

// 无锁 SPSC 队列——证明永远不会 data race
template <typename T, size_t N>
class SPSCQueue {
    T buffer_[N];
    std::atomic<size_t> write_pos_{0};
    std::atomic<size_t> read_pos_{0};
public:
    bool push(const T& item) {
        size_t w = write_pos_.load(std::memory_order_relaxed);
        size_t r = read_pos_.load(std::memory_order_acquire);  // ⑤ 关键 acquire
        if (w - r == N) return false;
        buffer_[w % N] = item;                                 // ④ 写入槽位
        write_pos_.store(w + 1, std::memory_order_release);    // ③ release
        return true;
    }
    bool pop(T& item) {
        size_t r = read_pos_.load(std::memory_order_relaxed);
        size_t w = write_pos_.load(std::memory_order_acquire);  // ② 关键 acquire
        if (r == w) return false;
        item = buffer_[r % N];                                   // ⑥ 读取槽位
        read_pos_.store(r + 1, std::memory_order_release);      // ① release
        return true;
    }
};
// 证明 push 写 buffer 和 pop 读 buffer 不冲突:
//  push④ (SB) push③(release) (SW) pop②(acquire) (SB) pop⑥
//  → push④ (HB) pop⑥ → 生产者写完、消费者才能读到 ≡ 无 data race ✅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

关键:acquire 的选择不是随意的——write_pos_ 的 acquire 和 read_pos_ 的 release 形成交叉配对。这是 SPSC 队列正确性的数学基础。


# 9. 所有序的汇编全景与性能对比

# 9.1 x86 vs ARM 的指令生成对比表

操作 C++ memory_order x86 指令 ARM 指令 相对开销
load relaxed mov ldr 基准 (1×)
load acquire mov (隐含有序) ldr; dmb ishld 2-3×
load seq_cst mov (x86 load 即 acquire) ldr; dmb ishld 2-3×
store relaxed mov str 基准 (1×)
store release mov (隐含有序) dmb ish; str 3-5×
store seq_cst xchg 或 mov+mfence dmb ish; str; dmb ish 5-10×
RMW relaxed/acq_rel/seq_cst lock xadd (全屏障) ldaddal (版本差异) 15-25×

# 9.2 benchmark——各内存序的代价量化

100 万次操作,ARM Cortex-A76:

操作 relaxed acquire/release seq_cst
load 1.2 ms 2.8 ms 2.8 ms
store 1.1 ms 4.2 ms 9.5 ms
fetch_add 14 ms 15 ms 15 ms

# 9.3 x86 上为什么 seq_cst load 等于 acquire load

x86 的硬件保证:Load 自带 Load-Load 和 Load-Store 有序。 所以 load(acquire) 和 load(seq_cst) 在 x86 上生成相同的 mov——不需要 fence。

但 store 不同——store(seq_cst) 需要 mfence 来保证 Store-Load 有序(Store Buffer 排空)。而 store(release) 不需要——x86 的 Store-Store 天然有序。


# 10. 综合案例串讲

# 10.1 案例真相揭晓

# 疑问 答案
① 六种内存序分别是什么? 第 2-7 章:relaxed / acquire / release / acq_rel / seq_cst / consume(废弃)
② relaxed 为什么不能用于互斥? 第 3 章:没有顺序保证——临界区代码可能在标志位之前对其他核可见
③ acquire-release 配对? 第 4/8 章:release SW acquire → release 之前的所有写 HB acquire 之后的读
④ acq_rel vs seq_cst? 第 5/6 章:acq_rel 局部屏障, seq_cst 全局总序——更贵
⑤ consume 为什么废弃? 第 7 章:编译器无法追踪依赖链——全部降级为 acquire
⑥ 汇编 vs 性能? 第 9 章:x86 上 relaxed=mov, seq_cst store=mfence, ARM 上 acquire=dmb ishld
⑦ 怎么选内存序? 第 10.2 决策树

案例①修复(DCL):把 relaxed 改为 acquire/release 配对。或直接用 C++11 保证的 static 局部变量。

案例②修复(滥用 acq_rel):write_pos.load 不需要 acquire——用 relaxed 即可。

# 10.2 内存序选择决策树

需要跨线程建立 happens-before 吗?
├─ 否 → relaxed
│    例:计数器 increment、引用计数递减、只有最终值有意义
│
├─ 是 → acquire-release 配对
│       大多数并发数据结构——无锁队列、消息传递、标志位
│
└─ 需要多个 atomic 变量的全局统一顺序?
    ├─ 是 → seq_cst
    │    例:同时依赖 x 和 y 的顺序、需要所有线程看到同一时间线
    └─ 否 → 回到 acquire-release
1
2
3
4
5
6
7
8
9
10
11

# 10.3 设计哲学回扣

哲学 1:内存序是 C++ 并发程序的「类型安全」——编译器不帮你检查,但运行时会崩

编译器不会告诉你「relaxed flag signal 没有 happens-before 保证」——它只看原子性,不看并发语义。内存序的正确性是程序员的逻辑责任——不是编译器的检查对象。 C++ 给了一个精确的证明系统(happens-before)——用它来验证你的并发代码,而不是靠「跑了 1000 次没崩」来验证。

哲学 2:acquire-release 是实现「最少屏障」的精确工具——不是 seq_cst 的子集

seq_cst 是安全毯——如果你不知道用哪个,用 seq_cst 至少是对的。但 acquire-release 让你在正确的条件下减少 60-80% 的 fence 开销。最优的并发代码不是「最强的内存序」——是「刚好够用的内存序」。 每个不必要的 fence 都是浪费的 CPU 周期。

哲学 3:happens-before 是并发正确的唯一数学标准——不是直觉

「会不会崩」?编译器回答不了。但 happens-before 可以。如果每个共享变量的访问都在 HB 关系的保护下——没有 data race。HB 是并发正确性的数学基础——把「我觉得没问题」变成「我证明了没问题」。 四个基础关系(SB+SW+依赖排序+线程间)构建了完整的传递闭包,这是 C++ 内存模型最优雅的部分。

哲学 4:consume 的失败是「理想优美 vs 工程可行性」的诚实案例

consume 在理论上是优美的——只追踪依赖链、不搞全屏障。但在 C++ 的复杂类型系统面前(别名、多态、指针链、间接引用)——编译器无法自信地判断安全的依赖链边界。标准委员会的选择是诚实的:承认做不到——并建议用 acquire 替代。 不是所有理论上可行的优化都能在工程中实现——consume 就是这条规则的注脚。

哲学 5:正确性第一、性能第二——先 seq_cst 写对,再降级

所有并发代码的推荐起始点:全部用 seq_cst。写对。然后用 happens-before 分析哪些操作用 weaker order 就够了。降级。测试。seq_cst 是原型——acquire-release 是优化——relaxed 是特化。永远从最强的内存序开始、向下优化——而不是从最弱的开始、向上修复。

# 10.4 速查表合集

六种内存序速查:

内存序 方向 典型用途 x86 额外开销
relaxed 无 计数器、引用计数 0
acquire ← 防后面的重排到前面 load flag 后在临界区读数据 0 (mov 即 acquire)
release → 防前面的重排到后面 store 数据后 store flag 0 (mov 即 release)
acq_rel ←→ 双向 RMW (fetch_add, CAS) 0 (lock 即 acq_rel)
seq_cst 全局总序 多 atomic 顺序依赖 +mfence (store)
consume 废弃 — —

下一篇:内存序的理论说清了。下一篇进入 42.atomic 原子操作原理——std::atomic 的内部实现、lock-free vs wait-free、CAS 与 ABA 问题、atomic<T> 对 T 的要求(为什么不能传 vector)、atomic_ref 给非 atomic 对象套上原子外壳。

上次更新: 2026/06/10, 11:13:41
C++内存模型基石
atomic原子操作原理

← C++内存模型基石 atomic原子操作原理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式