并发:Ordering 内存顺序约束与自旋锁应用

在 Rust 并发编程中,标准库的 std::sync::atomic::Ordering 枚举定义了 5 种内存顺序(Memory Ordering)。理解它们的核心,在于搞清楚原子操作如何与周围的普通内存读写协同工作。


一、核心定位:Ordering 到底在控制什么?

原子类型(如 AtomicBool、AtomicUsize)本身已经保证了单次操作的不可分割性(不会读写到一半的数据)。

Ordering 控制的不是单个变量的原子性,而是内存可见性与重排规则:

  1. 重排限制:约束编译器与 CPU 能否把该原子操作与其前后的其他内存读写交换执行顺序。
  2. 跨线程可见性:在多核 CPU 缓存体系下,建立跨线程的 Happens-Before(先行发生) 关系,确保一个线程写入的数据能按预期被另一个线程看到。

因此,选择 Ordering 的本质是在权衡:“当前这个原子操作,需要与周围的普通读写建立多强的先后顺序约束?”


二、5 种 Ordering 详解

                 【内存约束从弱到强】
Relaxed  ──►  Release(写)  ──►  Acquire(读)  ──►  AcqRel(读写)  ──►  SeqCst(全局)
  只有原子性    发布数据        接收数据         既收又发           全局统一顺序
    │ 无顺序      │ 屏障在上方    │ 屏障在下方     │ 双向屏障        │ 最强屏障

1. Relaxed(松散顺序)


2. Release(释放 / 发布)


3. Acquire(获取 / 同步)

💡 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)


5. SeqCst(Sequential Consistency,顺序一致性)


三、实战推演:为什么自旋锁需要这几种顺序?

以一个最简单的自旋锁实现为例:

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);
    }
}

四、选型决策流程

我们在日常编写并发代码时,可以通过以下步骤来选定合适的 Ordering:

                需要同步其他非原子数据/临界区吗?
                         │
           ┌─────────────┴─────────────┐
          否                           是
          │                            │
      [Relaxed]                  是纯读还是纯写?
  (如纯计数器、打点)                   │
                         ┌─────────────┴─────────────┐
                        读                           写
                         │                           │
                    [Acquire]                   [Release]
                 (加锁/等待信号)              (解锁/发布数据)
                         │                           │
                         └─────────────┬─────────────┘
                                  读改写 (RMW)
                                       │
                                   [AcqRel]
                              (状态机流转/中继传递)
  1. 独立原子变量(只关心计数或状态本身,无外部数据依赖)→ Relaxed
  2. 读操作(需要确保读到该变量后,能安全访问对应的数据)→ Acquire
  3. 写操作(需要确保修改完共享数据后,再发布状态标记)→ Release
  4. 复合操作 / RMW(既要感知前序状态,又要发布后续状态)→ AcqRel
  5. 多变量强共识 / 无法确定重排后果 → SeqCst(先保证正确性,后续结合基准测试再评估是否降级优化)

判断口诀

问自己三个问题,一步步缩小范围:

  1. 要不要和其他数据建立先后顺序?
    • 不要 → Relaxed
  2. 是读还是写?
    • 写(发布数据后打信号)→ Release
    • 读(读数据前确认信号)→ Acquire
  3. 是读改写(RMW),且处于传递链中段?
    • 是 → AcqRel
  4. 需要全局统一顺序 / 拿不准 / 性能不敏感?
    • → SeqCst