并发:用AtomicU32 位操作手撸读写锁

在基于 Rust 实现轻量级读写锁(RwLock)时,通常会使用一个 AtomicU32 来统一管理所有的锁状态。为了避免占用多个原子变量带来的开销,我们可以将 32 位整型划分成不同的区域:

在这种设计下,我们需要针对状态中的特定某一位或某几位进行精确修改,而不破坏其他位的数据。Rust 的 AtomicU32 提供了多种原子操作方法来实现这一目标。


一、fetch_or:原子置位(特定位置 1)

应用场景:写者排队标记

当写者准备获取锁时,第一步需要将 WRITER_WAITING 标志位置 1(阻断后续新读者的进入),同时不能影响已有的读者计数和写者持有状态。

// 仅将 WRITER_WAITING 对应的第 31 位置 1,其余 31 位保持不变
let old_state = self.state.fetch_or(WRITER_WAITING, Ordering::Release);
state(旧值)     = 0 0 0000...0000000110 (读者数=6)
WRITER_WAITING  = 1 0 0000...0000000000 (仅第31位为1)
────────────────────────────────────────────────────
state(新值)     = 1 0 0000...0000000110 (读者数=6,写者等待标志位=1)

二、fetch_and:原子清零(特定位置 0)

应用场景:写锁释放

写者释放锁时,需要同时清除 WRITER_HOLDING 和可能存在的 WRITER_WAITING 标志位。我们可以通过对标志位做按位取反(!),构造出一个“目标位为 0、其余位均为 1”的掩码。

// WRITER_HOLDING | WRITER_WAITING  -> 第 30、31 位为 1
// !(...)                           -> 第 30、31 位为 0,低 30 位全为 1
self.state.fetch_and(!(WRITER_HOLDING | WRITER_WAITING), Ordering::Release);
state(旧值)     = 1 1 0000...0000000000 (写者持有 + 写者等待)
!MASK           = 0 0 1111...1111111111 (高2位为0,低30位全为1)
────────────────────────────────────────────────────
state(新值)     = 0 0 0000...0000000000 (标志位全部清零)

三、fetch_add 与 fetch_sub:读者计数器增减

应用场景:读者加锁与解锁

// 读者加锁:读者计数 +1
self.state.fetch_add(1, Ordering::Acquire);

// 读者释放锁:读者计数 -1
self.state.fetch_sub(1, Ordering::Release);

四、compare_exchange:条件式原子状态转移(CAS)

应用场景:精确的状态切换与竞争控制

当写者确认“读者数量已归零,且无其他写者持有锁”时,需要将等待状态转换为持有状态:

// 仅在当前状态仍为 WRITER_WAITING 时,将其切换为 WRITER_HOLDING
let result = self.state.compare_exchange(
    WRITER_WAITING,
    WRITER_HOLDING,
    Ordering::Acquire,
    Ordering::Relaxed,
);

CAS 同样适用于复杂的自旋重试循环,用于处理无法通过单一 fetch_* 完成的多字段联动修改。


五、方法对比与选用总结

原子方法 底层位运算 / 逻辑 作用 读写锁典型应用
fetch_or(mask) state |= mask 将掩码中为 1 的对应位置 1 标记写者等待(开启阻断)
fetch_and(mask) state &= mask 将掩码中为 0 的对应位清 0 写锁释放(清除写标志)
fetch_add(n) / fetch_sub(n) state += n / state -= n 对低位连续计数区进行增减 读者进出时的计数统计
compare_exchange(cur, new) 条件赋值:if state == cur { state = new } 依赖当前状态的原子状态切换 写者由“等待”转为“持有”

六、核心设计原则

在单一原子整数上进行多字段状态切分时,核心逻辑可以归纳为两点:

  1. 置位使用 fetch_or:构造目标位为 1、其余位为 0 的掩码;
  2. 清位使用 fetch_and:构造目标位为 0、其余位为 1 的掩码(通常用 !MASK 构造)。

这些原子操作由 CPU 硬件指令(如 LOCK XADD、LOCK CMPXCHG、LOCK BTS/BTR 等)提供底层支持,确保“读取-修改-写回”过程作为一个不可分割的整体执行,从而在无额外锁开销的前提下保证多线程安全。