C++ 中的原子操作(std::atomic
)通过 memory order 标签控制内存访问的顺序和可见性,这些标签直接影响多线程程序的行为和性能。以下是六种 memory order 的核心区别和应用场景:
一、总览:按约束强度排序
标签 | 缩写 | 原子性 | 顺序一致性 | 写可见性 | 典型场景 |
---|---|---|---|---|---|
memory_order_relaxed |
R | ✅ | ❌ | ❌ | 计数器、统计数据 |
memory_order_consume |
C | ✅ | ❌ | 部分✅ | 依赖链数据 |
memory_order_acquire |
A | ✅ | ❌ | 全部✅ | 读锁、屏障 |
memory_order_release |
R | ✅ | ❌ | 全部✅ | 写锁、屏障 |
memory_order_acq_rel |
AR | ✅ | ❌ | 全部✅ | 原子读写(如 CAS) |
memory_order_seq_cst |
SC | ✅ | ✅ | 全部✅ | 默认选项,简单但性能较低 |
二、核心区别详解
1. memory_order_relaxed
(最弱)
- 特性:
- 仅保证原子性,不保证内存顺序。
- 允许编译器/CPU 自由重排序读写操作。
- 场景:
- 单线程内的原子操作。
- 无需跨线程同步的计数器(如引用计数)。
std::atomic<int> cnt(0);
void increment() {
cnt.fetch_add(1, std::memory_order_relaxed); // 仅保证原子性
}
2. memory_order_consume
(读取端)
- 特性:
- 保证当前线程中,所有依赖该原子变量的后续操作不会被重排序到该操作之前。
- 只影响有数据依赖的指令(如
y = x + 1
中的y
)。
- 场景:
- 优化
acquire
的性能,仅同步必要的数据。
- 优化
std::atomic<int*> ptr(nullptr);
int data;
void producer() {
data = 42;
ptr.store(&data, std::memory_order_release);
}
void consumer() {
int* p = ptr.load(std::memory_order_consume);
if (p) {
// 保证 data 已被写入(因为 p 依赖 ptr)
assert(*p == 42); // 不会触发
}
}
3. memory_order_acquire
(读取端)
- 特性:
- 保证当前线程中,所有后续读写操作不会被重排序到该操作之前。
- 确保读取到其他线程
release
操作后的最新值。
- 场景:
- 读取锁状态(如
std::mutex
的实现)。
- 读取锁状态(如
std::atomic<bool> ready(false);
int data;
void consumer() {
while (!ready.load(std::memory_order_acquire));
// 保证 data 已被写入
assert(data == 42); // 不会触发
}
4. memory_order_release
(写入端)
- 特性:
- 保证当前线程中,所有之前的读写操作不会被重排序到该操作之后。
- 确保所有写入对其他线程的
acquire
操作可见。
- 场景:
- 写入锁状态(如
std::mutex
的解锁)。
- 写入锁状态(如
void producer() {
data = 42;
ready.store(true, std::memory_order_release);
// 保证 data 先写入,ready 后写入
}
5. memory_order_acq_rel
(读写端)
- 特性:
- 同时具有
acquire
和release
的语义。 - 常用于原子的“读-改-写”操作(如
fetch_add
、compare_exchange
)。
- 同时具有
- 场景:
- 实现无锁数据结构(如自旋锁)。
std::atomic<int> lock(0);
void worker() {
while (lock.exchange(1, std::memory_order_acq_rel)) {
// 自旋等待锁释放
}
// 获得锁,执行临界区
lock.store(0, std::memory_order_release);
}
6. memory_order_seq_cst
(默认,最强)
- 特性:
- 提供全局顺序一致性(Total Order)。
- 所有线程看到的所有
seq_cst
操作顺序完全相同。 - 等价于
acq_rel
+ 全局同步屏障。
- 场景:
- 默认选项,简单易用但性能开销最大。
std::atomic<bool> x(false), y(false);
std::atomic<int> z(0);
void thread1() {
x.store(true, std::memory_order_seq_cst);
}
void thread2() {
y.store(true, std::memory_order_seq_cst);
}
void thread3() {
while (!x.load(std::memory_order_seq_cst));
if (y.load(std::memory_order_seq_cst)) z++;
}
void thread4() {
while (!y.load(std::memory_order_seq_cst));
if (x.load(std::memory_order_seq_cst)) z++;
}
// 最终 z 一定为 2(所有线程看到的操作顺序一致)
三、关键对比
对比点 | acq_rel |
seq_cst |
---|---|---|
全局顺序一致性 | ❌ | ✅ |
性能 | 较高(无全局同步) | 较低(强制全局同步) |
多原子变量的顺序保证 | 仅保证单个变量的操作顺序 | 保证所有变量的操作顺序一致 |
对比点 | consume |
acquire |
---|---|---|
同步范围 | 仅依赖该变量的操作 | 所有后续操作 |
性能 | 更高(仅同步必要数据) | 较低(同步所有数据) |
适用场景 | 数据依赖链(如指针、配置) | 通用同步(如锁、屏障) |
四、使用建议
- 优先使用
seq_cst
:除非性能要求极高且能确保正确性。 - 性能敏感场景:
- 计数器:
relaxed
- 单生产者-单消费者:
release/acquire
- 依赖链数据:
consume
- 计数器:
- 避免过度优化:错误使用弱顺序可能导致难以调试的内存序错误。
- 测试工具:使用 ThreadSanitizer(TSan)等工具检测内存序错误。
五、总结
理解 memory order 的关键在于区分:
- 原子性(Atomicity):保证操作不可分割。
- 顺序性(Ordering):控制指令重排序。
- 可见性(Visibility):确保写操作对其他线程可见。
合理选择 memory order 能在正确性和性能之间取得平衡,是编写高效无锁代码的核心技能。