并发:用AtomicU32 位操作手撸读写锁
在基于 Rust 实现轻量级读写锁(RwLock)时,通常会使用一个 AtomicU32 来统一管理所有的锁状态。为了避免占用多个原子变量带来的开销,我们可以将 32 位整型划分成不同的区域:
- 低 30 位(Bit 0 ~ Bit 29):存放并发读者的计数器(支持最大约 10 亿读者)。
- 第 30 位(Bit 30):
WRITER_HOLDING,标记当前是否有写者正在持有锁。 - 第 31 位(Bit 31):
WRITER_WAITING,标记当前是否有写者在排队等待。
在这种设计下,我们需要针对状态中的特定某一位或某几位进行精确修改,而不破坏其他位的数据。Rust 的 AtomicU32 提供了多种原子操作方法来实现这一目标。
一、fetch_or:原子置位(特定位置 1)
- 方法机制:对当前原子值与传入的掩码执行按位或(
|),写回结果并返回修改前的旧值。 - 位运算规律:
- 任何位与
1做|运算,结果必为1; - 任何位与
0做|运算,保持原样。
- 任何位与
应用场景:写者排队标记
当写者准备获取锁时,第一步需要将 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)
- 方法机制:对当前原子值与传入的掩码执行按位与(
&),写回结果并返回修改前的旧值。 - 位运算规律:
- 任何位与
0做&运算,结果必为0; - 任何位与
1做&运算,保持原样。
- 任何位与
应用场景:写锁释放
写者释放锁时,需要同时清除 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:读者计数器增减
- 方法机制:对原子变量进行加减法。
- 位运算规律:
读者计数存放在整型的低 30 位,且占据最低的连续区间。只要读者数量不溢出第 30 位,直接加减1就只会改变低 30 位的计数,高位的写者标志不会受到任何干扰。
应用场景:读者加锁与解锁
// 读者加锁:读者计数 +1
self.state.fetch_add(1, Ordering::Acquire);
// 读者释放锁:读者计数 -1
self.state.fetch_sub(1, Ordering::Release);
四、compare_exchange:条件式原子状态转移(CAS)
- 方法机制:比较并交换(Compare-And-Swap)。仅当当前状态等于预期值
current时,才将其原子地更新为new;如果在此期间状态被其他线程修改,则操作失败并返回最新的状态。
应用场景:精确的状态切换与竞争控制
当写者确认“读者数量已归零,且无其他写者持有锁”时,需要将等待状态转换为持有状态:
// 仅在当前状态仍为 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 } |
依赖当前状态的原子状态切换 | 写者由“等待”转为“持有” |
六、核心设计原则
在单一原子整数上进行多字段状态切分时,核心逻辑可以归纳为两点:
- 置位使用
fetch_or:构造目标位为1、其余位为0的掩码; - 清位使用
fetch_and:构造目标位为0、其余位为1的掩码(通常用!MASK构造)。
这些原子操作由 CPU 硬件指令(如 LOCK XADD、LOCK CMPXCHG、LOCK BTS/BTR 等)提供底层支持,确保“读取-修改-写回”过程作为一个不可分割的整体执行,从而在无额外锁开销的前提下保证多线程安全。