并发:Ordering 内存顺序约束与自旋锁应用
在 Rust 并发编程中,标准库的 std::sync::atomic::Ordering 枚举定义了 5 种内存顺序(Memory Ordering)。理解它们的核心,在于搞清楚原子操作如何与周围的普通内存读写协同工作。
一、核心定位:Ordering 到底在控制什么?
原子类型(如 AtomicBool、AtomicUsize)本身已经保证了单次操作的不可分割性(不会读写到一半的数据)。
Ordering 控制的不是单个变量的原子性,而是内存可见性与重排规则:
- 重排限制:约束编译器与 CPU 能否把该原子操作与其前后的其他内存读写交换执行顺序。
- 跨线程可见性:在多核 CPU 缓存体系下,建立跨线程的 Happens-Before(先行发生) 关系,确保一个线程写入的数据能按预期被另一个线程看到。
因此,选择 Ordering 的本质是在权衡:“当前这个原子操作,需要与周围的普通读写建立多强的先后顺序约束?”
二、5 种 Ordering 详解
【内存约束从弱到强】
Relaxed ──► Release(写) ──► Acquire(读) ──► AcqRel(读写) ──► SeqCst(全局)
只有原子性 发布数据 接收数据 既收又发 全局统一顺序
│ 无顺序 │ 屏障在上方 │ 屏障在下方 │ 双向屏障 │ 最强屏障
1. Relaxed(松散顺序)
- 适用操作:读(
load)、写(store)、读改写(RMW,如fetch_add)。 - 语义:仅保证原子性,不提供任何顺序约束。编译器和 CPU 可以自由将周围的代码与该操作重排。
- 使用场景:只关心变量本身的值累加或状态变更,不依赖它去同步其他共享数据。
use std::sync::atomic::{AtomicU64, Ordering}; static METRICS_COUNTER: AtomicU64 = AtomicU64::new(0); // 纯打点统计,不保护任何其他数据,Relaxed 性能最高 METRICS_COUNTER.fetch_add(1, Ordering::Relaxed);
2. Release(释放 / 发布)
- 适用操作:写(
store)或读改写(RMW)中的写部分。 - 语义:“发布数据”。确保位于该操作之前的所有内存读写,都不能被重排到该操作之后。
- 直观理解:当前线程完成了共享数据的准备工作,最后通过一次
Release写打上标记,把之前的所有改动一并“发布”出去。
3. Acquire(获取 / 同步)
- 适用操作:读(
load)或读改写(RMW)中的读部分。 - 语义:“获取数据”。确保位于该操作之后的所有内存读写,都不能被重排到该操作之前。
- 直观理解:当前线程必须先读到有效信号,才能开始处理后续依赖的数据。
💡 Acquire 与 Release 的协同配对
Release 与 Acquire 通常成对使用,用于在线程间传递数据所有权:
use std::sync::atomic::{AtomicBool, Ordering};
static mut SHARED_DATA: u64 = 0;
static READY: AtomicBool = AtomicBool::new(false);
// 线程 A:生产者
fn producer() {
unsafe { SHARED_DATA = 42; } // 1. 准备数据
READY.store(true, Ordering::Release); // 2. 发布标记(1 不会被重排到 2 之后)
}
// 线程 B:消费者
fn consumer() {
// 3. 获取标记(4 不会被重排到 3 之前)
while !READY.loadAcquire {}
// 4. 安全读取数据:必定能看到 42
let val = unsafe { SHARED_DATA };
println!("val: {val}");
}
4. AcqRel(Acquire + Release)
- 适用操作:仅适用于**读改写(Read-Modify-Write, RMW)**操作(如
fetch_add、swap、compare_exchange)。 - 语义:同时具备 Acquire 和 Release 语义。
- 读的部分使用
Acquire:能看到之前其他线程通过Release发布的数据。 - 写的格式使用
Release:保证自己修改前后的改动对后续线程可见。
- 读的部分使用
- 使用场景:处于数据同步链条的中继环节(既要接收上游状态,又要向下游发布新状态),例如无锁队列出入队节点的流转、并发状态机转换等。
5. SeqCst(Sequential Consistency,顺序一致性)
- 适用操作:读、写、读改写。
- 语义:最强约束。在具备
Acquire/Release语义的基础上,额外保证:所有线程观察到的所有SeqCst操作都有一个全局统一的执行先后顺序(就像所有线程都在按同一个全局时钟串行执行)。 - 使用场景:复杂的无锁数据结构中,多个线程需要对多个不同的原子变量的变更顺序达成完全一致的共识。
- 代价:在大多数现代硬件(如 x86 的 store-load 屏障、ARM 的全屏障指令)上会产生更重的硬件屏障开销。
三、实战推演:为什么自旋锁需要这几种顺序?
以一个最简单的自旋锁实现为例:
use std::sync::atomic::{AtomicBool, Ordering};
pub struct SpinLock {
locked: AtomicBool,
}
impl SpinLock {
pub const fn new() -> Self {
Self { locked: AtomicBool::new(false) }
}
pub fn lock(&self) {
// compare_exchange 需要两个 Ordering:成功时、失败时
while self.locked.compare_exchange_weak(
false,
true,
Ordering::Acquire, // 成功获取锁:建立 Acquire 屏障
Ordering::Relaxed // 未获取锁:仅自旋重试,不涉及临界区数据,Relaxed 即可
).is_err() {
std::hint::spin_loop();
}
}
pub fn unlock(&self) {
// 释放锁:临界区内的所有修改必须在解锁前全部落盘
self.locked.store(false, Ordering::Release);
}
}
- 加锁成功(
Acquire):进入临界区后,我们要读写受保护的业务数据。Acquire保证临界区内的操作不会被重排到加锁完成之前,同时能看到上一任持锁者在临界区内的所有修改。 - 加锁失败(
Relaxed):没拿到锁,我们只是在等待,不涉及任何受保护数据的读写,不需要额外的内存屏障,纯原子检查即可。 - 解锁(
Release):临界区内的数据已经改完,Release确保临界区内的写操作全部就绪,再把锁置为false发布给下一个加锁者。
四、选型决策流程
我们在日常编写并发代码时,可以通过以下步骤来选定合适的 Ordering:
需要同步其他非原子数据/临界区吗?
│
┌─────────────┴─────────────┐
否 是
│ │
[Relaxed] 是纯读还是纯写?
(如纯计数器、打点) │
┌─────────────┴─────────────┐
读 写
│ │
[Acquire] [Release]
(加锁/等待信号) (解锁/发布数据)
│ │
└─────────────┬─────────────┘
读改写 (RMW)
│
[AcqRel]
(状态机流转/中继传递)
- 独立原子变量(只关心计数或状态本身,无外部数据依赖)
Relaxed - 读操作(需要确保读到该变量后,能安全访问对应的数据)
Acquire - 写操作(需要确保修改完共享数据后,再发布状态标记)
Release - 复合操作 / RMW(既要感知前序状态,又要发布后续状态)
AcqRel - 多变量强共识 / 无法确定重排后果
SeqCst(先保证正确性,后续结合基准测试再评估是否降级优化)
判断口诀
问自己三个问题,一步步缩小范围:
- 要不要和其他数据建立先后顺序?
- 不要 →
Relaxed
- 不要 →
- 是读还是写?
- 写(发布数据后打信号)→
Release - 读(读数据前确认信号)→
Acquire
- 写(发布数据后打信号)→
- 是读改写(RMW),且处于传递链中段?
- 是 →
AcqRel
- 是 →
- 需要全局统一顺序 / 拿不准 / 性能不敏感?
- →
SeqCst
- →