mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2412 字
7 分钟
内存排序与内存屏障
2026-03-26

你写了一段多线程代码:线程 A 先写 data 再写 flag,线程 B 等待 flag 后读 data。逻辑上,flag=1data 一定已经就绪。但在某些 CPU 上,线程 B 可能看到 flag=1 却读到 data 的旧值——这不是 bug,而是 CPU 的内存排序(Memory Ordering)行为。

内存排序是并发编程中最微妙的话题之一。理解它,才能写出正确的多线程代码;不理解它,你的程序在 x86 上正确但在 ARM 上可能出错。

一、为什么会有内存排序?#

1.1 编译器重排#

编译器在优化时可能重排指令:

// 源代码
data = 42;
flag = 1;
// 编译器可能重排为(如果 data 和 flag 没有依赖关系)
flag = 1;
data = 42;

编译器只保证单线程语义不变——在单线程视角下,两种顺序结果相同。但多线程视角下,另一个线程可能看到 flag=1data 还是 0。

1.2 CPU 重排#

CPU 的 Store Buffer(见第 7 章)也会导致内存重排:

sequenceDiagram participant CoreA as 核心 A participant SB_A as Store Buffer A participant Cache as 缓存/主存 participant SB_B as Store Buffer B participant CoreB as 核心 B CoreA->>SB_A: data = 42(写入 Store Buffer) CoreA->>SB_A: flag = 1(写入 Store Buffer) Note over SB_A: 两个写入都在 Store Buffer 中 SB_A->>Cache: flag = 1(先刷到缓存) Note over CoreB: 看到 flag = 1 CoreB->>Cache: 读取 data Cache-->>CoreB: data = 0 (data 还没刷到缓存) SB_A->>Cache: data = 42(后刷到缓存)

1.3 四种内存重排#

重排类型含义示例
Store-Store两个写操作重排data=42; flag=1flag=1; data=42
Load-Load两个读操作重排读 flag 后读 data → 先读 data 后读 flag
Store-Load写后读重排flag=1; return data → 先读 data 后写 flag
Load-Store读后写重排读 data 后写 flag → 先写 flag 后读 data

二、内存模型:从最强到最弱#

2.1 内存模型谱系#

graph LR SC["顺序一致性<br/>Sequential Consistency<br/>最强"] --> TSO["TSO<br/>Total Store Order<br/>x86"] TSO --> PSO["PSO<br/>Partial Store Order<br/>SPARC"] PSO --> RELAXED["弱序模型<br/>Relaxed<br/>ARM / RISC-V"] style SC fill:#e8f5e9,stroke:#2e7d32 style TSO fill:#fff9c4,stroke:#f9a825 style RELAXED fill:#ffcdd2,stroke:#c62828

2.2 顺序一致性(SC)#

最严格的模型:所有核心看到所有操作的全局顺序,且顺序与程序顺序一致。

在 SC 模型下,不存在任何内存重排。但 SC 严重限制硬件优化,现代 CPU 都不实现 SC。

2.3 x86 TSO(Total Store Order)#

x86 的内存模型是 TSO,允许的唯一重排是 Store-Load

重排类型x86 TSO原因
Store-Store不允许写入按 FIFO 顺序从 Store Buffer 刷出
Load-Load不允许读取按程序顺序
Store-Load允许Store Buffer 导致写后读可能重排
Load-Store不允许读取不会延迟后续写入
Note

x86 的 TSO 是”较强”的内存模型——只允许 Store-Load 重排。这意味着在 x86 上,很多并发代码”碰巧”能正确运行,但移植到 ARM 上可能出错。

2.4 ARM/RISC-V 弱序模型#

ARM 和 RISC-V 的内存模型允许所有四种重排:

重排类型ARM/RISC-V原因
Store-Store允许Store Buffer 不保证 FIFO
Load-Load允许Invalidate Queue 导致读重排
Store-Load允许Store Buffer
Load-Store允许读写可重叠

2.5 三种模型的对比#

属性SCx86 TSOARM 弱序
Store-Store保证保证不保证
Load-Load保证保证不保证
Store-Load保证不保证不保证
Load-Store保证保证不保证
需要屏障SFENCE/LFENCE/MFENCEDMB/DSB/ISB

三、内存屏障#

3.1 内存屏障的类型#

屏障类型作用防止的重排
LoadLoad之前的读在之后的读之前完成Load-Load
StoreStore之前的写在之后的写之前完成Store-Store
LoadStore之前的读在之后的写之前完成Load-Store
StoreLoad之前的写在之后的读之前完成Store-Load
全屏障(Full)以上全部全部

3.2 x86 的内存屏障指令#

指令作用对应屏障
SFENCEStore 屏障StoreStore
LFENCELoad 屏障LoadLoad + LoadStore
MFENCE全屏障全部四种

在 x86 TSO 下,大多数场景不需要屏障——因为只有 Store-Load 可能重排。只有需要防止 Store-Load 重排时才需要 MFENCE。

3.3 ARM 的内存屏障指令#

指令作用对应屏障
DMB(Data Memory Barrier)数据内存屏障全屏障
DSB(Data Synchronization Barrier)数据同步屏障全屏障 + 等待完成
ISB(Instruction Synchronization Barrier)指令同步屏障刷新流水线

ARM 的 DMB 比 x86 的 MFENCE 更频繁使用——因为 ARM 允许更多重排。

3.4 屏障的代价#

CPUMFENCE/DMB 延迟说明
x86 (Intel)~20-40 周期排空 Store Buffer
ARM (Cortex-A76)~20-50 周期排空 Store Buffer + Invalidate Queue
ARM (Cortex-A53)~30-80 周期顺序核心,屏障开销更大

四、C++ memory_order#

4.1 六种内存序#

C++11 的原子操作支持六种内存序:

memory_order保证对应硬件
relaxed无排序保证无屏障
consume数据依赖排序很少硬件支持,通常等同于 acquire
acquire之后的读/写不能重排到此之前LoadLoad + LoadStore 屏障
release之前的读/写不能重排到此之后LoadStore + StoreStore 屏障
acq_relacquire + release全屏障
seq_cst全局顺序一致全屏障 + 全局顺序协议

4.2 Release-Acquire 语义#

最常用的模式:Release 写入 + Acquire 读取构成同步关系。

#include <atomic>
int data = 0;
std::atomic<int> flag{0};
// 线程 A(生产者)
void producer() {
data = 42; // 普通写入
flag.store(1, std::memory_order_release); // Release 写入
// 保证 data=42 在 flag=1 之前对其他线程可见
}
// 线程 B(消费者)
void consumer() {
while (flag.load(std::memory_order_acquire) != 1) // Acquire 读取
;
// flag=1 的 Acquire 保证之后读到 data 的最新值
assert(data == 42); // 一定成功!
}
sequenceDiagram participant A as 线程 A participant Flag as flag participant B as 线程 B A->>A: data = 42 A->>Flag: store(1, release) Note over A: Release 保证之前的写入可见 B->>Flag: load(acquire) == 1 Note over B: Acquire 保证之后的读取看到 Release 之前的写入 B->>B: assert(data == 42)

4.3 memory_order 的硬件映射#

C++ memory_orderx86 实现ARM 实现
relaxed普通 mov普通 ldr/str
acquire普通 mov(x86 读有 acquire 语义)ldr + DMB ISH
release普通 mov(x86 写有 release 语义)DMB ISH + str
acq_rel普通 movDMB ISH + str + DMB ISH
seq_cstmov + MFENCE(store)/ lock 前缀DMB ISH + str + DMB ISH
Note

x86 的 TSO 模型使得 acquire/release 几乎免费——普通读写就具有 acquire/release 语义。ARM 的弱序模型则需要显式屏障,这就是为什么同一份并发代码在 ARM 上比 x86 慢。

4.4 双重检查锁定模式#

class Singleton {
static std::atomic<Singleton*> instance;
static std::mutex mtx;
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
};

五、Linux 内核的内存屏障#

5.1 内核屏障 API#

API作用对应硬件
barrier()编译器屏障无硬件指令
smp_rmb()读屏障LFENCE / DMB
smp_wmb()写屏障SFENCE / DMB
smp_mb()全屏障MFENCE / DMB
smp_load_acquire()Acquire 读取取决于架构
smp_store_release()Release 写入取决于架构

5.2 编译器屏障 vs 硬件屏障#

// 编译器屏障:阻止编译器重排,但不阻止 CPU 重排
barrier(); // Linux 内核
__asm__ __volatile__("" ::: "memory"); // GCC
// 硬件屏障:同时阻止编译器和 CPU 重排
smp_mb(); // Linux 内核
Warning

编译器屏障和硬件屏障的区别经常被忽视。在单线程代码中,编译器屏障足够;但在多线程代码中,必须使用硬件屏障。

六、常见的内存排序陷阱#

6.1 陷阱 1:Dekker 算法在 ARM 上失败#

// Dekker 互斥算法(在 x86 上正确,在 ARM 上可能失败)
std::atomic<int> flag0{0}, flag1{0}, turn{0};
// 线程 0
flag0.store(1, std::memory_order_relaxed);
while (flag1.load(std::memory_order_relaxed)) {
if (turn.load(std::memory_order_relaxed) != 0) {
flag0.store(0, std::memory_order_relaxed);
while (turn.load(std::memory_order_relaxed) != 0);
flag0.store(1, std::memory_order_relaxed);
}
}
// 临界区...

在 ARM 的弱序模型下,relaxed 操作可能被重排,导致两个线程同时进入临界区。修复:使用 memory_order_acq_relseq_cst

6.2 陷阱 2:自旋锁的实现#

// 错误的自旋锁(在 ARM 上可能失败)
void lock(std::atomic<int> &mutex) {
while (mutex.exchange(1, std::memory_order_relaxed)) {
// relaxed 不保证临界区内的写入对其他线程可见
}
}
// 正确的自旋锁
void lock(std::atomic<int> &mutex) {
while (mutex.exchange(1, std::memory_order_acquire)) {
// acquire 保证临界区内的读取看到最新的值
}
}
void unlock(std::atomic<int> &mutex) {
mutex.store(0, std::memory_order_release);
// release 保证临界区内的写入对其他线程可见
}

七、动手实验#

7.1 实验 1:Store-Load 重排(x86)#

#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
int x = 0, y = 0;
int r1 = 0, r2 = 0;
void *thread_a(void *arg) {
x = 1; // Store
r1 = y; // Load
return NULL;
}
void *thread_b(void *arg) {
y = 1; // Store
r2 = x; // Load
return NULL;
}
int main() {
int count = 0;
for (int i = 0; i < 1000000; i++) {
x = y = 0;
pthread_t t1, t2;
pthread_create(&t1, NULL, thread_a, NULL);
pthread_create(&t2, NULL, thread_b, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
if (r1 == 0 && r2 == 0) {
count++; // Store-Load 重排!
}
}
printf("Store-Load 重排次数: %d / 1000000\n", count);
// x86: 可能出现少量重排(Store Buffer 导致)
// ARM: 可能出现更多重排
}

7.2 实验 2:内存屏障的效果#

#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
int data = 0;
atomic_int flag = 0;
// 无屏障版本
void *producer_no_barrier(void *arg) {
data = 42;
atomic_store(&flag, 1, memory_order_relaxed);
return NULL;
}
// Release-Acquire 版本
void *producer_release(void *arg) {
data = 42;
atomic_store(&flag, 1, memory_order_release);
return NULL;
}
void *consumer_acquire(void *arg) {
while (atomic_load(&flag, memory_order_acquire) != 1);
printf("data = %d\n", data); // Release-Acquire: 一定是 42
return NULL;
}

八、x86 / ARM / RISC-V 内存模型对比#

8.1 三种架构的允许重排#

重排类型x86-64 (TSO)ARMv8RISC-V
Store-Store不允许允许允许
Load-Load不允许允许允许
Store-Load允许允许允许
Load-Store不允许允许允许

x86 的 TSO 是”强序”模型——只有 Store-Load 一种重排。ARM 和 RISC-V 都是弱序模型,允许所有四种重排,但两者的具体语义有细微差异。

8.2 ARMv8 vs RISC-V 的差异#

维度ARMv8RISC-V
模型名称”Weakly Ordered” + “Multi-copy Atomicity""Weak Memory Ordering” (RVWMO)
多副本原子性有(Store 对所有核心同时可见)有(Same-address 原子性)
数据依赖保证(地址/控制/数据依赖不被重排)保证(addr/ctrl/data dependency)
屏障指令DMB / DSB / ISBFENCE (rw, rw)
Acquire/ReleaseLDAR / STLR 指令FENCE.RW, RW / FENCE.R, RW 等
Note

ARMv8 引入了 LDAR(Load-Acquire)和 STLR(Store-Release)指令,比 ARMv7 的 DMB 屏障更轻量。RISC-V 的 FENCE 指令可以细粒度控制读写方向(如 FENCE.RW,RW = 全屏障,FENCE.R,R = LoadLoad 屏障)。

8.3 同一代码在不同架构上的行为#

// 经典的 Message Passing 测试
int data = 0;
int flag = 0;
// 线程 A
void writer() {
data = 42; // Store 1
flag = 1; // Store 2
}
// 线程 B
void reader() {
if (flag == 1) { // Load 1
assert(data == 42); // Load 2
}
}
架构assert 可能失败?原因
x86-64不会Store-Store 不重排,flag=1 一定在 data=42 之后可见
ARMv8 (无屏障)可能Store-Store 可重排,flag=1 可能先于 data=42 可见
ARMv8 (用 STLR)不会STLR 保证 Store-Store 不重排
RISC-V (无 FENCE)可能Store-Store 可重排
RISC-V (FENCE.W,W)不会FENCE.W,W 保证 Store-Store 不重排

九、Acquire-Release 语义详解#

9.1 Acquire 语义#

Acquire 操作保证:之后的读写不能被重排到 Acquire 之前

┌──────────────────────────────┐
Acquire │ 之后的读/写不能越过此线向上 │
└──────────────────────────────┘
↑ 之前的操作可以自由重排
  • 对应屏障:LoadLoad + LoadStore
  • 典型用途:读取标志位后,保证后续的数据读取看到最新值

9.2 Release 语义#

Release 操作保证:之前的读写不能被重排到 Release 之后

↓ 之后的操作可以自由重排
┌──────────────────────────────┐
Release │ 之前的读/写不能越过此线向下 │
└──────────────────────────────┘
  • 对应屏障:LoadStore + StoreStore
  • 典型用途:写入数据后,保证标志位的写入在数据写入之后可见

9.3 Release-Acquire 同步对#

当 Release 写入和 Acquire 读取配对时,它们构成一个同步关系(synchronizes-with):

sequenceDiagram participant A as 线程 A participant M as 共享变量 participant B as 线程 B A->>A: data = 42 (普通写) A->>M: flag.store(1, release) Note over A: Release 保证 data=42 在 flag=1 之前 B->>M: flag.load(acquire) == 1 Note over B: Acquire 保证之后看到 Release 之前的所有写入 B->>B: assert(data == 42)

关键点:Release-Acquire 不是双向屏障。Release 只保证”之前的操作不泄漏”,Acquire 只保证”之后的操作不提前”。它们配对才能建立完整的同步。

9.4 常见的 Acquire-Release 使用模式#

// 模式 1:自旋锁
void lock(std::atomic<int> &mutex) {
while (mutex.exchange(1, std::memory_order_acquire)) {
// acquire: 临界区内的读取不会提前到锁获取之前
}
}
void unlock(std::atomic<int> &mutex) {
mutex.store(0, std::memory_order_release);
// release: 临界区内的写入不会延迟到锁释放之后
}
// 模式 2:一次性初始化
std::atomic<int> initialized{0};
int data;
void init() {
data = compute(); // 普通写
initialized.store(1, std::memory_order_release); // Release
}
void use() {
if (initialized.load(std::memory_order_acquire)) { // Acquire
use_data(data); // 一定看到 compute() 的结果
}
}
// 模式 3:生产者-消费者队列(单生产者单消费者)
std::atomic<int> tail{0}, head{0};
int buffer[SIZE];
void produce(int val) {
int t = tail.load(std::memory_order_relaxed);
buffer[t % SIZE] = val;
tail.store(t + 1, std::memory_order_release); // Release: buffer 写入在 tail 更新之前
}
int consume() {
int h = head.load(std::memory_order_relaxed);
while (tail.load(std::memory_order_acquire) <= h); // Acquire: 读取 buffer 在 tail 读取之后
int val = buffer[h % SIZE];
head.store(h + 1, std::memory_order_release);
return val;
}

十、无锁编程模式#

10.1 SeqLock(顺序锁)#

SeqLock 是一种读多写少的无锁模式,Linux 内核广泛使用:

// SeqLock:读端无需获取锁
std::atomic<int> seq{0};
int data_a, data_b;
// 写端(需要互斥)
void write(int a, int b) {
seq.fetch_add(1, std::memory_order_release); // 序号变为奇数 → 写入中
data_a = a;
data_b = b;
seq.fetch_add(1, std::memory_order_release); // 序号变为偶数 → 写入完成
}
// 读端(无锁)
bool read(int *a, int *b) {
int s1, s2;
do {
s1 = seq.load(std::memory_order_acquire);
*a = data_a; // 读取数据(可能正在被写入)
*b = data_b;
s2 = seq.load(std::memory_order_acquire);
} while (s1 != s2 || s1 & 1); // 序号变化或写入中 → 重试
return true;
}

10.2 RCU(Read-Copy-Update)思想#

RCU 允许读者无锁访问,写者创建副本后原子替换指针:

// 简化的 RCU 模式
struct Data {
int values[100];
};
std::atomic<Data*> current{nullptr};
// 读者:无锁
void reader() {
Data* ptr = current.load(std::memory_order_acquire);
use(ptr->values); // 使用期间 ptr 不会被释放
// 通过 grace period 保证 ptr 在所有读者退出后才释放
}
// 写者:创建副本
void writer() {
Data* old = current.load(std::memory_order_acquire);
Data* new_data = malloc(sizeof(Data));
*new_data = *old; // 复制
new_data->values[0] = 42; // 修改副本
current.store(new_data, std::memory_order_release); // 原子替换
// 等待 grace period 后释放 old
synchronize_rcu();
free(old);
}
Warning

无锁编程的正确性极难保证。以上代码是简化版本,生产环境需要处理 ABA 问题、内存回收、grace period 等复杂问题。详见第 15 章:无锁编程

十一、小结#

上一章了解了缓存一致性与 MESI 协议。

概念要点对软件的影响
四种重排Store-Store, Load-Load, Store-Load, Load-Store不同 ISA 允许的重排不同
x86 TSO只允许 Store-Load 重排并发代码在 x86 上更容易正确
ARM 弱序允许所有四种重排需要更多显式屏障
Release-Acquire最常用的同步模式C++ memory_order_release/acquire
内存屏障代价20-80 周期频繁使用屏障会降低性能
编译器屏障只阻止编译器重排多线程需要硬件屏障

下一步SIMD 与向量化——如何用一条指令同时处理多个数据?SSE/AVX/NEON 指令如何加速你的代码?

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

内存排序与内存屏障
https://blog.souloss.com/posts/cpu-architecture/memory-ordering/
作者
Souloss
发布于
2026-03-26
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
指令集架构:x86/ARM/RISC-V
CPU与计算机体系结构 指令集架构是软硬件之间的契约。本章对比 x86、ARM、RISC-V 三大 ISA 的设计哲学,解析 CISC 与 RISC 的本质差异,探讨指令编码、扩展机制与生态系统的深层逻辑。
2
系列导读
CPU与计算机体系结构 本系列从后端工程师的视角出发,自底向上剖析现代 CPU 的核心机制——指令集架构、流水线、分支预测、乱序执行、缓存层次、内存一致性、SIMD、TLB、NUMA、无锁编程,每章配有可运行的代码实验与性能分析,让你从「写代码」进阶到「理解代码如何在 CPU 上跑」。
3
NUMA 架构
CPU与计算机体系结构 多插槽系统中,每个 CPU 插槽有自己的本地内存。访问本地内存快,访问远端内存慢——这就是 NUMA。全面剖析解析 NUMA 拓扑、延迟差异、numactl 工具,以及如何编写 NUMA 感知的应用程序。
4
CPU 全景:为什么后端工程师需要理解 CPU
CPU与计算机体系结构 从后端工程师的视角俯瞰现代 CPU——内存墙、摩尔定律的终结、多核趋势、微架构概览,以及为什么理解 CPU 体系结构是性能优化的必修课。
5
乱序执行与推测执行
CPU与计算机体系结构 乱序执行是现代 CPU 突破指令级并行极限的关键技术。一网打尽解析寄存器重命名、ROB、保留站的工作机制,揭示推测执行的原理与 Spectre/Meltdown 攻击的技术根源。