在多线程编程中,编译器和 CPU 为了追求极致性能,会对指令进行重排序 (Reordering)。内存排序(Memory Ordering)就是我们用来告诉编译器和 CPU:“这里的顺序不能乱,必须按我规定的来”。
Rust 通过 std::sync::atomic::Ordering 枚举提供了从弱到强的几种排序约束。
1. Relaxed (松散顺序)
- 强度:最弱。
- 保证:只保证当前原子操作本身是原子的。
- 不保证:不保证多线程间的操作顺序,也不保证其他普通变量的读写顺序。
- 适用场景:全局计数器、统计数据(不依赖其他数据)。
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
// 线程 A
COUNTER.fetch_add(1, Ordering::Relaxed);
// 线程 B
let count = COUNTER.load(Ordering::Relaxed);
2. Release & Acquire (释放与获取)
这是并发编程中最常用的组合,用于建立**先行发生(Happens-Before)**关系。它们必须成对使用。
Release(写):通常用于发送方。
- 写屏障:确保 Release 操作之前的所有内存写入(包括普通变量)都已完成。
- 告诉 CPU:“在我修改这个变量之前,前面所有的数据准备工作都必须落盘”,不能重排到我后面
Acquire(读):通常用于接收方。
- 读屏障:确保在 Acquire 操作之后的所有内存读取都能看到 Release 线程写入的数据。
- 告诉 CPU:“再我读取这个原子变量之后,后面所有的逻辑才能执行,不能重排到我前面。”
经典场景:数据传递(Flag 模式)
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]); // 普通数据
let ready = Arc::new(AtomicBool::new(false)); // 原子 Flag
let data_clone = data.clone();
let ready_clone = ready.clone();
// 生产者线程
thread::spawn(move || {
// 1. 准备数据 (普通写)
// thread::sleep(...);
// 2. 发送信号 (Release)
// 保证:上面的数据准备一定会在这一步之前完成,不会被乱序下去
ready_clone.store(true, Ordering::Release);
});
// 消费者线程
// 3. 等待信号 (Acquire)
while !ready.load(Ordering::Acquire) {
std::hint::spin_loop();
}
// 4. 读取数据 (普通读)
// 保证:因为上面用了 Acquire,这里的读取一定能看到线程 A 准备好的数据
assert_eq!(data[0], 1);
}
3. AcqRel
- 语义:同时包含 Acquire 和 Release
- 场景:通常用于
fetch_add或compare_and_exchange这种既读又写的操作。你希望读取之时保证之前的顺序,写入时保证之后的顺序。
4. SeqCst (顺序一致性)
强度:最强。
保证:除了包含 Acquire/Release 的所有保证外,还保证全局唯一的执行顺序。
代价:性能开销最大(通常会阻止 CPU 缓存优化,强制同步所有核心的缓存)。
场景:当多个线程同时对多个原子变量进行操作,且你非常关心它们在时间线上的绝对先后顺序时。如果你不确定用什么,用 SeqCst 通常是安全的,但可能较慢。
总结
一句话记忆:Relaxed 是为了不乱序自己;Acquire/Release 是为了不乱序别人(保护普通数据);SeqCst 是为了大家都不乱。