本文深入解析std::atomic的三大核心属性:原子性、可见性与顺序性,强调仅用atomic不等于线程安全。内存序是解决可见性与顺序性的关键,通过happens-before语义屏蔽编译器与CPU重排差异。六种内存序按强度分层,从relaxed到seq_cst,需根据场景选择:默认用seq_cst保证跨平台安全;发布-订阅用release/acquire;计数器可用relaxed。volatile不能替代atomic。硬件层面,x86的TSO模型使release/acquire开销小,而ARM等弱模型需屏障指令,导致同一代码在不同平台行为迥异。核心观点:内存序不是性能开关,而是确保多线程间状态传递正确性的语言级保障。
一、问题起点:原子性不等于可见性,也不等于顺序性
std::atomic解决三件不同的事:
原子性:读-改-写不会被拆开,不会出现“撕裂值”。
可见性:一个线程的写入,什么时候能被另一个线程看到。
顺序性:多个内存操作之间的先后关系,其他线程是否也能观察到同样顺序。
很多人以为“用了std::atomic就线程安全了”,这是错的。原子性只解决第 1 点。内存序解决的是第 2、3 点。
二、为什么需要内存序:两层重排
没有内存序约束时,两件事会重排:
1. 编译器重排
data = 42; ready = 1;编译器可以变成:
ready = 1; data = 42;只要单线程执行结果不变,编译器就有权这么做。多线程下,另一个线程可能先看到ready == 1,再读data,得到 0。
2. CPU 重排
现代 CPU 是流水线、乱序执行、写缓冲(store buffer)的。
x86 是 TSO(Total Store Order):
Load 不重排 Load
Store 不重排 Store
只允许 Store → Load 重排
ARM / RISC-V / POWER 是弱内存模型:
Load-Load、Load-Store、Store-Store、Store-Load 都可能重排
所以同一份 C++ 代码:
x86 上“碰巧对”
ARM 上直接出数据竞争
内存序就是用来屏蔽这些差异的语言级抽象。
三、happens-before:内存序的真正语义
C++不让你去想“CPU 先执行了哪条指令”,而是定义:
如果 Ahappens-before B,那么 A 的副作用对执行 B 的线程一定可见。
happens-before 来自:
同一线程内的程序顺序
release写 与 同一原子变量上的acquire读 之间的 synchronizes-withmutex::unlock→mutex::lockthread启动、join、条件变量通知等
内存序的作用,就是告诉编译器和 CPU:
“在这一点前后,哪些普通内存访问不许跨过来。”
四、六种内存序,按强度分层
1.memory_order_relaxed
只保证原子性,不保证顺序。
counter.fetch_add(1, std::memory_order_relaxed);适用:
引用计数自增
统计计数器
不依赖“其他变量也同时可见”的场景
不适用:
用原子变量做“数据发布”
生产者-消费者握手
2.memory_order_acquire(读端)
while (!ready.load(std::memory_order_acquire)) {} // 后面读 data 一定看到生产者释放前的内容 int x = data;语义:
本次 load 之后的读写,不能被重排到本次 load 之前
如果读到了别人 release 写入的值,就建立 synchronizes-with
3.memory_order_release(写端)
data = 42; ready.store(true, std::memory_order_release);语义:
本次 store 之前的读写,不能被重排到本次 store 之后
消费者若 acquire 到这个值,就能看到
data = 42
经典发布模型:
// 生产者 payload = ...; flag.store(true, release); // 消费者 while (!flag.load(acquire)) ; use(payload); // 安全4.memory_order_acq_rel
用于 read-modify-write:
val.fetch_add(1, std::memory_order_acq_rel);既是 acquire 又是 release。常用于:
自旋锁
无锁栈/队列里的 CAS/RMW
5.memory_order_seq_cst
默认内存序,最强:
有 acquire/release 语义
所有 seq_cst 操作之间存在一个全局总顺序
所有线程看到这些原子操作的顺序完全一致
x.store(1, std::memory_order_seq_cst); y.store(1, std::memory_order_seq_cst);其他线程不会看到“矛盾的全序视角”。
代价:
x86 上可能生成
mfence或xchgARM 上需要
dmb ish多核高竞争时性能明显下降
6.memory_order_consume
基于数据依赖的弱同步。C++26起弃用,主流编译器直接按 acquire 处理,实际工程基本不用。
五、为什么不能用“volatile”代替
volatile只阻止编译器优化,不保证:
CPU 不重排
多核可见顺序
与其他变量的顺序关系
volatile不是并发原语。std::atomic+ 正确内存序才是。
六、硬件映射:同一份 C++,不同汇编
x86-64
ready.store(true, memory_order_release); // 通常就是 mov [ready], 1 // 不需要 mfence flag.load(memory_order_acquire); // 通常就是 mov rax, [flag]x86 TSO 已经隐含了 StoreStore / LoadLoad 顺序,所以 release/acquire 在 x86 上“几乎免费”,主要约束编译器。
ready.store(true, memory_order_seq_cst); // 可能生成: // mov [ready], 1 // mfence // 或 xchg [ready], regARM64
ready.store(true, release); // stlr flag.load(acquire); // ldar seq_cst store; // dmb ish + storeARM 允许大量重排,所以 release/acquire 会真正变成屏障指令。
这就是“x86 上没问题,ARM 上一炸再炸”的根本原因。
七、典型错误:消息传递用 relaxed
// 错误 data = 42; ready.store(true, memory_order_relaxed); // 另一线程 if (ready.load(memory_order_relaxed)) { assert(data == 42); // 可能失败 }relaxed 不建立 happens-before。ready == true被看到时,data的写入可能还没传播出去。
正确写法:
data = 42; ready.store(true, memory_order_release); if (ready.load(memory_order_acquire)) { assert(data == 42); // 安全 }八、选型原则
默认用 seq_cst
正确、可推理、跨平台安全。
发布数据用 release / acquire
无锁队列、SPSC 管道、标志位握手的首选。
独立计数器用 relaxed
不发布其他状态,只求自增原子性。
不要过早优化内存序
大多数无锁 bug 来自“我以为 relaxed 够用”。
多个变量要一起可见,用 mutex 或把数据放进指针后 release 指针
原子变量只能保证“这一个对象”的原子性,不能自动保护一堆普通变量。
九、一句话本质
内存序不是“让程序更快的开关”,而是告诉编译器和 CPU:
“哪些内存操作必须像单线程里那样,保持先后关系和跨线程可见性。”
没有内存序,原子变量只是“不会撕裂的变量”;
有了内存序,它才成为线程之间传递状态的通道。