跳转至

原子

原文链接: https://marabos.nl/atomics/atomics.html.

英文: Atomic.

原子 (Atomic) 一词源于希腊语 ἄτομος, 意为不可再分的.
所有其他并发原语 (例如互斥体和条件变量) 都是使用原子操作实现的.

Rust 提供多种原子类型 (std::sync::atomic), 但并非所有平台都支持所有的原子类型.

Load 和 Store 操作

原子类型最基础的操作是 loadstore, 对应读和写操作.

以 AtomicI32 的函数签名为例:

impl AtomicI32 {
    pub fn load(&self, order: Ordering) -> i32;
    pub fn store(&self, val: i32, order: Ordering);
}

文中举了一个名为 "Stop Flag" 的例子, 展示了仅通过 loadstore 操作就能实现的功能.
该例子中, 原子变量只在一个线程里被读取 (load), 在另一个线程里被写入 (store), 所以不可能导致冲突.

Fetch-and-Modify 操作

但如果需要基于原子变量的值进行修改, 只使用 loadstore 操作, 就可能导致竞争1.
原子类型提供多种 Fetch-and-Modify 操作, 比如用于加法操作的 fetch_add. 可以用于对原子类型做简单的算术运算, 同时保持原子性.

以 AtomicI32 的函数签名为例:

impl AtomicI32 {
    pub fn fetch_add(&self, val: i32, order: Ordering) -> i32;
    pub fn fetch_sub(&self, val: i32, order: Ordering) -> i32;
    pub fn fetch_or(&self, val: i32, order: Ordering) -> i32;
    pub fn fetch_and(&self, val: i32, order: Ordering) -> i32;
    pub fn fetch_nand(&self, val: i32, order: Ordering) -> i32;
    pub fn fetch_xor(&self, val: i32, order: Ordering) -> i32;
    pub fn fetch_max(&self, val: i32, order: Ordering) -> i32;
    pub fn fetch_min(&self, val: i32, order: Ordering) -> i32;
    pub fn swap(&self, val: i32, order: Ordering) -> i32; // "fetch_store"
}

Compare-and-Exchange 操作

最灵活和通用的操作是 compare_exchange, 该操作也是其他 Fetch-and-Modify 操作的实现基础.

以 AtomicI32 的函数签名为例:

impl AtomicI32 {
    pub fn compare_exchange(
        &self,
        current: i32,
        new: i32,
        success: Ordering,
        failure: Ordering,
    ) -> Result<i32, i32>;
}

该方法是一个原子操作 (对应 CAS (compare-and-swap) 原子指令), 若原子变量的值等于 current, 将原子变量的值改为 new.
交换 (exchange 或 swap) 主要体现在该方法会返回原子变量原本的值, 虽然这个值理论上等于 current.

下面是 fetch_add 的一种可能的实现:

impl AtomicI32 {
    pub fn fetch_add(&self, val: i32, order: Ordering) -> i32 {
        let mut current = self.load(Relaxed);
        loop {
            let new = current + val;
            match self.compare_exchange(current, new, Relaxed, Relaxed) {
                Ok(v) => return v,
                Err(v) => current = v,
            }
        }
    }
}

上文仅通过 loadstore 操作实现了例子 "Lazy Initialization", 但这种实现方式存在以下限制:

  1. 初始化函数的返回值必须是恒定的: 这样才能在有竞争的情况下, 保证中间和最终结果是正确的.
  2. 可能调用多次初始化函数: 会产生不必要的性能开销.

通过 compare_exchange 操作, 可以实现例子 "Lazy One-Time Initialization". 与原先的例子相比, 该方法不存在上述的第一条限制.

如果还需要消除第二条限制, 则需要堵塞线程, 避免多次调用初始化函数. Rust 标准库通过以下结构体提供该功能:

  • std::sync::Once: 确保一段代码只被执行一次. 如果这段代码用于初始化某个静态可变变量, 则使用者需编写 unsafe 代码.
  • std::sync::OnceLock: 确保一个变量只被初始化一次. 相比 std::sync::Once, 其目的更明确, 只用于初始化变量, 所以封装了 unsafe 代码, 使用者无需编写 unsafe 代码.

  1. 是竞争 (race) 而非数据竞争 (data race), 数据竞争是未定义行为, 不会出现在 safe Rust 中. 

评论