Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

内存模型与 memory barrier

一句话

你以为下面这段代码"不可重排" —— 它真的可被重排

a = 1;
b = 2;

CPU、编译器、SSD 控制器都有权把它们写成 b = 2; a = 1;,只要在单线程视角下看起来一致。这就是语言内存模型:"单线程可观察的行为"。要约束它,必须插 memory barrier。这一章把 x86 弱/强内存模型、ARM 弱内存模型、C11 atomic order、Java volatile、Go sync/atomic 的语义都对齐到同一抽象,让你不再把 memory barrier 当玄学。

1. 三种 memory 重排来源

源码:
    a = 1; b = 2;
编译器:
   - register allocation: 让 a 走 stack 不优先;
   - 优化重组: 看上去单线程行为相同的指令可以乱序.
CPU:
   - store buffer: 写发出 CPU 顺序不一定 = 实际到 cache 顺序;
   - invalidate queue: CPU 收到的 invalidate 可能 wait 一个忙指令才被处理;
cache/NUMA 互连:
   - 多 socket 的 cache 一致性 message 顺序不可控.

每跨一层都可能是"reordering source"。所以多线程下 instruction order 在所有 CPU 上的全局序是不可观察的、不可假设的、不可依赖的。除非用 memory barrier。

2. 经典反例: Peterson's lock 失效

flag[0] = true;  turn = 1;
while (flag[1] && turn == 1) ;
// critical section
flag[0] = false;
flag[1] = true;  turn = 0;
while (flag[0] && turn == 0) ;
// critical section
flag[1] = false;

单线程看对;多核 CPU 没有逻辑屏障的话,因为 flag[0]=true; turn=1 可能被重排到 turn=1 先 → turn=0 写入了;两个都看到对方 flag=true 但 turn 是自己 → 双双进 critical section,peterson 失败。

修复:flag/turn 的写必须配 store-store barrier,turn 的读必须配 load-load barrier。

3. x86 内存模型: TSO

TSO = Total Store Order. x86 相对不算太弱:

  • Load-Load 不重排 (但 load 可看到 older store);
  • Load-Store 不重排;
  • Store-Store 不重排 (但是发出 store buffer 可能延迟);
  • Store-Load 可重排 ⇒ 这就是 x86 唯一需要的 barrier。

mfence 指令强制 store-load barrier。lock cmpxchg 等原子指令本身也强 带 fence 等价的语义。所以 x86 上写 lock-free 算法比 ARM 友好得多。

x86 弱点例子:

a = 1;     // store
r = b;     // load
// 在 x86 上, 可能 r 看到 b 的旧值,但 a 的 store 也可能在 r 完成后才发 cache。
// 因为 store buffer。

4. ARM 内存模型: weak / RMO

ARM 是弱内存模型——所有 4 种 reorder 都被允许

  • Load-Load;
  • Load-Store;
  • Store-Store;
  • Store-Load。

ARM 处理器为了一致性,需要显式 dmb sy / dmb ish / dmb nsh (DSB/DMB/ISB) 等屏障。这就是为什么 ARM code 的 lock-free 代码充斥 barrier, 而 x86 几乎不需要.

弱内存模型给硬件设计师更多自由(指令级并行 + cache pipeline 更深),但给软件工程师更难调 - C++ std::atomic 在 ARM 上比 x86 多了几条 dmb 指令。

5. C11 memory order

C11 atomic 提供一组细粒度语义:

memory_order_relaxed    不要求 ordering, 只有 atomic
memory_order_consume    像 acquire 但 dependency chain only (实用不被建议, 一般 compiler downgrades 到 relaxed)
memory_order_acquire    前续 read/write 必须在 load 完后
memory_order_release    后续 read/write 必须在 store 完前
memory_order_acq_rel    acquire + release (compare_exchange 同时)
memory_order_seq_cst    顺序一致, 默认最强

常见用法:

// 单生产者单消费者
atomic_store_explicit(&ready, 1, memory_order_release);
while (!atomic_load_explicit(&ready, memory_order_acquire)) ;
// acquire 之后看到的旧 store 都已可见

x86 上 release = 普通 store;acquire = 普通 load 加上 compiler barrier (因为 TSO 不重排);ARM 上 release = dmb ish st / acquire = dmb ish ld

6. Java volatile / Go sync/atomic

Java volatile 等价 seq_cst:写后读可见且跨线程同步顺序一致。JMM 用 happens-before 关系建模。

volatile boolean ready = false;
// thread 1: a = 1; ready = true;
// thread 2: while (!ready); int r = a;  // r 必为 1

Go sync/atomic 提供 C11 风格 API:

atomic.StoreInt64(&ready, 1)  // seq_cst 默认
atomic.LoadInt64(&ready)

Go 内存模型用 happens-before 关系,channel send / sync.Mutex.Unlock / atomic 都建立 happens-before 边。

7. 实战模式: 单生产者单消费者 SPSC queue

SPSC 是 lock-free 最经典模式:

struct SPSC {
    atomic<size_t> head;  // producer writes
    atomic<size_t> tail;  // consumer writes
    T buf[N];
};

void push(T x) {
    size_t h = head.load(memory_order_relaxed);
    size_t t = tail.load(memory_order_acquire);    // 同步 consumer 已消费
    if (h - t == N) return;                         // full
    buf[h % N] = x;
    head.store(h + 1, memory_order_release);        // 同步 consumer 看见 data
}

T pop() {
    size_t t = tail.load(memory_order_relaxed);
    size_t h = head.load(memory_order_acquire);    // 同步 producer 已生产
    if (t == h) return {};                          // empty
    T x = buf[t % N];
    tail.store(t + 1, memory_order_release);        // 同步 producer 看见 data
    return x;
}

acquire / release 是"消息传递"的关键:

  • producer 用 release store: "我已写好数据 + 你能读";
  • consumer 用 acquire load: 我读完你的 head 才能读 buf 中数据.

否则: consumer 看到 head 推进了, 但 buf[i] 仍是旧数据 — 经典 bug.

8. ARM 上的实测代价

// ARM Cortex-A76 @2.4 GHz
 acquire load:~ 3 ns (dmb-ish ld)
 release store:~ 5 ns (dmb-ish st)
 seq_cst store + load:~ 10 ns
 没 barrier 包: 1 ns 提议

x86 类似 ~ 1-2 ns. ARM 弱内存模型 + cache 让 atomic 操作比 x86 配置更深.

9. 调试重排 bug

memory reorder bug 特征:

  • 偶尔出现, 不稳定; -v 单线程 debug 不复现;
  • valgrind / helgrind 不一定能抓 (但 tsan 抓 more then);
  • 改成 _acq/order_seq_cst 后 OK ⇒ 大概率是 reorder.

Google ThreadSanitizer 处理这个, 它 modeling happens-before 关系并报 race. 谛走 lock-free 必备 tsan.

10. 多语言同一操作

语言acquire / release 表达
C/C++11atomic_load/store_explicit
RustOrdering::Acquire / Release / SeqCst
Goatomic.Load/Store + sync
Javavolatile, final, synchronized
Python(单线程, 无意义)

抽象一致: 同一组 happens-before 概念在不同语言里换名字重物化.

11. 这章带走的东西

  • 内存重排来源: 编译器优化 + CPU store buffer + invalidate queue;
  • x86 TSO 仅允许 store-load 重排, ARM 全部允许;
  • C11 memory_order_acquire/release 是 SPSC "消息传递"基本;
  • ARM dmb 加 2-5 ns, x86 1-2 ns 默认 seq_cst 默认;
  • tsan 必须: lock-free + 缺 barrier bug 几乎肯定测不到的 silent.

下一节 → lock-free/wait-free 数据结构