C 语言内存屏障与原子操作
更新时间:2026-08-26。本文是
languages/c/主题专家层文档。多线程 C 程序里,i++不是原子的、编译器会重排你的代码、CPU 会乱序执行——这三件事加起来,让"多线程共享变量"成了最容易翻车的地方。本文讲清 C 语言层面的原子操作与内存屏障,衔接底层的内存模型原理。
本文要回答的问题
i++为什么不是原子的?两个线程同时i++会丢更新吗?- C11 的
_Atomic和 GCC 的__atomic怎么用?内存序参数怎么选? volatile能替代原子操作吗?内存屏障(barrier)又是干什么的?
一、为什么需要原子操作
1.1 i++ 不是原子的
int counter = 0;
void *worker(void *arg) {
for (int i = 0; i < 100000; i++)
counter++; // 危险!读-改-写 三步,不是原子操作
}counter++ 在汇编层是 mov(读)+ add(加)+ mov(写)三步。两个线程交错执行时,会"读旧值→各自加一→写回",导致丢更新(最终 counter 小于 200000)。
1.2 编译器重排 + CPU 乱序
即使单条指令是原子的,编译器和 CPU 也可能重排内存访问顺序:
// 你写的顺序
flag = 1;
data = 42;
// 编译器/CPU 可能重排成
data = 42;
flag = 1;单线程无所谓,但多线程下另一个线程看到 flag==1 时,data 可能还没写——这就是内存序问题(详见 内存模型与重排)。
二、C11 原子类型
2.1 _Atomic 关键字
C11 引入了原子类型:
#include <stdatomic.h>
_Atomic int counter = 0; // 原子整型
void *worker(void *arg) {
for (int i = 0; i < 100000; i++)
atomic_fetch_add(&counter, 1); // 原子自增
}atomic_fetch_add 保证"读-改-写"是原子的,不会被其他线程打断。
2.2 内存序(memory order)
原子操作可以指定内存序,控制"这个操作和前后操作能否被重排":
| 内存序 | 含义 | 使用场景 |
|---|---|---|
memory_order_seq_cst | 顺序一致(默认,最强) | 不确定时用这个 |
memory_order_relaxed | 只保证原子性,不保证顺序 | 纯计数器(顺序无关) |
memory_order_acquire | 读:后续访问不重排到此之前 | 配合 release 构成同步 |
memory_order_release | 写:之前的访问不重排到此之后 | 配合 acquire 构成同步 |
memory_order_acq_rel | 读改写:兼具 acquire+release | RMW 操作(如 CAS) |
atomic_fetch_add(&counter, 1, memory_order_relaxed); // 纯计数,可用 relaxed经验:先用默认的 seq_cst 保证正确,确认性能瓶颈后再按需放宽到 relaxed/acquire/release。
三、GCC 内建原子(__atomic / __sync)
不想用 C11 时,GCC 提供内建函数:
// __atomic 系列(推荐)
int old = __atomic_add_fetch(&counter, 1, __ATOMIC_SEQ_CST);
int cur = __atomic_load_n(&counter, __ATOMIC_ACQUIRE);
__atomic_store_n(&counter, 0, __ATOMIC_RELEASE);
// __sync 系列(旧,功能弱)
int old = __sync_fetch_and_add(&counter, 1);| 系列 | 特点 |
|---|---|
__atomic_* | GCC 4.7+,支持内存序参数,推荐 |
__sync_* | 旧接口,只支持全序,不推荐新代码 |
四、内存屏障(barrier)
屏障是"强制不跨过这条线重排"的指令:
// GCC 内建屏障
__sync_synchronize(); // 完整屏障(mfence 语义)
__atomic_thread_fence(__ATOMIC_SEQ_CST); // C11 屏障
// 编译器屏障:只阻止编译器重排,不阻止 CPU 乱序
__asm__ volatile("" ::: "memory"); // 经典 compiler barrier| 屏障类型 | 阻止什么 | 指令 |
|---|---|---|
| 编译器屏障 | 编译器重排 | 无(纯编译期) |
| 硬件完整屏障 | CPU 重排 | mfence(x86) |
| 读屏障 / 写屏障 | 单向重排 | lfence / sfence |
典型场景:无锁队列里,生产者写完数据后 release 屏障 + 写 flag,消费者 acquire 屏障 + 读 flag,保证"看到 flag 就一定能看到数据"(详见 无锁队列)。
五、volatile 不能替代原子操作
这是最常见的误解:
| 特性 | volatile | 原子操作 |
|---|---|---|
| 阻止编译器优化该变量 | ✅ | ✅ |
| 保证"读-改-写"原子性 | ❌ | ✅ |
| 保证内存序(防重排) | ❌ | ✅ |
| 适用场景 | 内存映射 IO、信号处理标志 | 多线程共享变量 |
结论:volatile 只告诉编译器"每次都要真的读写内存,别缓存",它不保证原子性、不保证内存序。多线程共享变量必须用原子操作。
// 错误:用 volatile 做多线程同步
volatile int flag = 0; // 不保证原子性,也不保证其他线程及时看到
// 正确:用原子操作
_Atomic int flag = 0;六、与本站主线衔接
- 内存模型原理:重排、store buffer、屏障的底层机制,见 L4 内存重排。
- 缓存一致性:原子操作依赖 MESI 协议保证缓存一致,见 L3 缓存一致性。
- 无锁编程:基于原子操作的锁无关数据结构,见 无锁队列。
- 并发与线程:C 语言的 pthread 多线程基础,见 并发编程。
一句话总结
多线程共享变量必须用原子操作(C11 _Atomic / GCC __atomic),i++ 不原子、编译器会重排、volatile 救不了;先用 seq_cst 保证正确,再按需用 relaxed/acquire/release 放宽内存序,屏障则用来强制"数据先于 flag 可见"。