Rust in Everything
基于 lock_api 0.4.14

给自己的锁套外壳

主线位置:主线 1 的 raw 层换成你自己的。外壳(Mutex<R, T> 与守卫)原样复用,lock / unlock 里不再停车,而是自旋。读完 raw trait 与契约 再看这一章。

flowchart LR
  s1["1 定义 raw 类型<br/>只放锁状态"] --> s2["2 unsafe impl RawMutex<br/>INIT、GuardMarker、lock、try_lock、unlock"]
  s2 --> s3["3 导出类型别名<br/>Spinlock&lt;T&gt; = lock_api::Mutex&lt;RawSpinlock, T&gt;"]
  s3 --> got["免费得到:守卫、map、try_lock、is_locked、<br/>const new、serde、arc_lock …"]
  s2 -.-> owe["自己担保:互斥、Acquire / Release、<br/>GuardMarker 选对"]

文档里的三步

lock_api 的 crate 文档(lib.rs 第 14–63 行)先列三步:

  1. 定义 raw 锁类型,「This should only contain the lock state, not any data protected by the lock」。
  2. 为它实现 RawMutex。
  3. 把 lock_api::Mutex 和 lock_api::MutexGuard 导出成类型别名。

然后给出完整例子:

use lock_api::{RawMutex, Mutex, GuardSend};
use std::sync::atomic::{AtomicBool, Ordering};

// 1. Define our raw lock type
pub struct RawSpinlock(AtomicBool);

// 2. Implement RawMutex for this type
unsafe impl RawMutex for RawSpinlock {
    const INIT: RawSpinlock = RawSpinlock(AtomicBool::new(false));

    // A spinlock guard can be sent to another thread and unlocked there
    type GuardMarker = GuardSend;

    fn lock(&self) {
        // Note: This isn't the best way of implementing a spinlock, but it
        // suffices for the sake of this example.
        while !self.try_lock() {}
    }

    fn try_lock(&self) -> bool {
        self.0
            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
            .is_ok()
    }

    unsafe fn unlock(&self) {
        self.0.store(false, Ordering::Release);
    }
}

// 3. Export the wrappers. This are the types that your users will actually use.
pub type Spinlock<T> = lock_api::Mutex<RawSpinlock, T>;
pub type SpinlockGuard<'a, T> = lock_api::MutexGuard<'a, RawSpinlock, T>;

逐项对应上一章的契约:

  • unsafe impl:签字担保互斥。这里靠的是 compare_exchange(false, true) 只会有一个线程成功。
  • Acquire / Release:try_lock 成功时用 Acquire,unlock 用 Release,上一个持锁者在临界区里的写入对下一个持锁者可见。CAS 失败那一侧用 Relaxed,失败时没有拿到锁,也就不需要看到任何东西。内存序的分类见 crossbeam 卷 内存序分类。0.4.14 的 trait 文档没有写这条要求,它是实现者自己要知道的。
  • GuardMarker = GuardSend:注释写明理由,「A spinlock guard can be sent to another thread and unlocked there」。unlock 只是一次 store,不关心是哪个线程执行的。
  • is_locked 没有写,用的是 trait 的默认实现(try_lock + unlock)。

文档自己承认 while !self.try_lock() {} 不是好的自旋锁写法:每一轮都是一次 CAS,等锁的线程不停地以写的方式争抢同一条缓存行。下面 spin 的写法是常见的改进。

spin 0.9.9 是怎么做的

registry 里 spin-0.9.9 默认打开 lock_api feature(依赖 lock_api = "0.4",在 crate 里改名为 lock_api_crate)。src/lib.rs 第 163–197 行导出:

pub mod lock_api {
    pub type Mutex<T> = lock_api_crate::Mutex<crate::Mutex<()>, T>;
    pub type MutexGuard<'a, T> = lock_api_crate::MutexGuard<'a, crate::Mutex<()>, T>;
    pub type RwLock<T> = lock_api_crate::RwLock<crate::RwLock<()>, T>;
    // RwLockReadGuard / RwLockWriteGuard / RwLockUpgradableReadGuard 同理
}

raw 锁不是另写的类型,而是 spin 自己的 Mutex<()>:一把保护「空数据」的锁,正好满足「只放锁状态」。src/mutex/spin.rs 第 353–377 行为 SpinMutex<(), R> 实现 RawMutex(src/mutex.rs 第 316–340 行为外层的 Mutex<(), R> 再实现一次,转发给内层):

#[cfg(feature = "lock_api")]
unsafe impl<R: RelaxStrategy> lock_api_crate::RawMutex for SpinMutex<(), R> {
    type GuardMarker = lock_api_crate::GuardSend;

    const INIT: Self = Self::new(());

    fn lock(&self) {
        // Prevent guard destructor running
        core::mem::forget(Self::lock(self));
    }

    fn try_lock(&self) -> bool {
        // Prevent guard destructor running
        Self::try_lock(self).map(core::mem::forget).is_some()
    }

    unsafe fn unlock(&self) {
        self.force_unlock();
    }

    fn is_locked(&self) -> bool {
        Self::is_locked(self)
    }
}
  • lock 调用 spin 自己的 lock() 拿到 spin 的守卫,再 mem::forget 掉,锁保持锁住;所有权转交给 lock_api 的守卫,由它在析构时调 unlock → force_unlock(一次 store(false, Release),spin.rs 第 218–220 行)。
  • GuardMarker 同样是 GuardSend。
  • is_locked 覆盖成一次 load(Relaxed)(第 206–208 行),不走默认的「试锁一下」。

spin 自己的 lock()(第 176–195 行)是 test-and-test-and-set:

while self
    .lock
    .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
    .is_err()
{
    // Wait until the lock looks unlocked before retrying
    while self.is_locked() {
        R::relax();
    }
}

CAS 失败后不立即重试,而是只读地等锁「看起来空了」,每轮调一次 R::relax()(默认的 Spin 策略是 spin_loop_hint,即 CPU 的自旋提示指令),再去 CAS。只读的等待让缓存行可以在多个核上以共享状态存在,不会每一轮都把它抢成独占。parking_lot 的 SpinWait 走的是另一条路:自旋几轮后让出线程,再之后停车(SpinWait),和 crossbeam Backoff 的对照见 Backoff。

spin 的 RwLock 只实现了 RawRwLock、RawRwLockUpgrade、RawRwLockDowngrade 三个 trait(raw trait 与契约 的表),所以 spin::lock_api::RwLock 上没有 try_read_for、read_recursive 这类方法。

实测:编译并测通

exp-parking_lot/custom-lock/,依赖 lock_api =0.4.14、spin =0.9.9,cargo test --offline --release。RawSpinlock 照抄文档例子(只把 GuardMarker 放在一个 feature 开关后面,供反向验证用)。8 项测试全部通过:

测试做什么结果
wrapper_counts_exactly8 线程各 *lock.lock() += 1 十万次,Spinlock<u64>恰好 800 000
spin_crate_wrapper_counts_exactly同上,spin::lock_api::Mutex<u64>恰好 800 000
raw_spinlock_never_overlaps只用 raw API:8 线程各进 1 万次临界区,进门 inside.fetch_add(1),返回值不为 0 就记一次重叠,临界区里自旋 20 次 spin_loop,出门 fetch_sub0 次
raw_spin_crate_never_overlaps同上,raw 锁换成 spin::Mutex<()>0 次
broken_lock_is_detected(反向)同上,raw 锁换成 lock 什么都不等的 BrokenRawLock75 857 / 79 904 / 79 365 次(三次运行,共 80 000 次进入)
guard_api_extrasmap 到 Vec 的第一个元素再改写;映射守卫析构后 is_locked() 为假通过
guard_is_send_with_guard_send编译期断言 SpinlockGuard<u64> 和 spin 的守卫是 Send通过;--features nosend 把标记换成 GuardNoSend 后编译失败(错误全文在 GuardSend 与 send_guard)
sizessize_ofSpinlock<()> 1、Spinlock<u64> 16,spin 的两个也是 1、16;MutexGuard<u64> 8、MappedMutexGuard<u64> 16、MappedMutexGuard<[u8]> 24

两点说明:

  • 「重叠计数」只用原子变量,所以坏锁那一组不会产生数据竞争;坏锁也没有装进 lock_api::Mutex,因为一旦装进去,两个线程就会同时拿着同一个值的 &mut,那本身就是未定义行为,测出来的数字没有意义。这一组的作用是证明重叠计数能发现「不互斥」,所以前两组的 0 才有分量。
  • 计数恰好 800 000 只说明这次运行没有丢更新,不能证明内存序写对了:把 Acquire / Release 改成 Relaxed,这个测试在某台机器上照样可能通过。内存序要靠推理或 loom 这类模型检查工具,见 crossbeam 卷 怎么验证并发代码。

外壳替你做了什么,不能替你保证什么

套上外壳后,三十来行 raw 代码换来 lock_api 的整套 API:RAII 守卫、try_lock、is_locked、const fn new、map / try_map、unlocked、into_inner / get_mut、Debug / Default / From,开 feature 还有 serde、arc_lock(外壳与守卫)。实现了 RawMutexFair / RawMutexTimed 就自动多出 unlock_fair、bump、try_lock_for。

外壳不检查、也无法检查的:

事项为什么外壳管不了本例
互斥只能相信 unsafe implCAS 保证;本章的重叠计数实验验证
内存序trait 签名里没有 Ordering;0.4.14 文档也没写Acquire / Release,靠推理
GuardMarker 选对选 GuardSend 等于声明「任何线程都能解锁」store(false) 不看线程,成立
不重入外壳不记录持有者同线程第二次 lock() 永远自旋
等待策略外壳只调 lock(),不管里面怎么等纯自旋:持锁线程被调度走时,等待者空转整个时间片
公平同上无任何保证

最后两行是自旋锁本身的取舍,不是 lock_api 的问题:在用户态、线程会被抢占的环境里,持锁线程被换下 CPU 时,所有等待者都在空烧。这正是 parking_lot 要「自旋一会儿就停车」的原因。

想要停车而不是自旋,raw 层可以直接用 parking_lot_core 的 park / unpark_one 写:用一个原子字表示锁状态,拿不到锁时 park(addr, validate, ...),解锁发现有人在等时 unpark_one(addr, callback)。协议和回调约束见 park 与四种 unpark,完整的「1 字节 mutex」和计数信号量放在 11 动手。

小结

  • 三步:只放锁状态的 raw 类型、unsafe impl RawMutex、两个类型别名;文档例子编译、测试全部通过,8 线程计数恰好 80 万。
  • 例子里的 Acquire / Release 和 GuardSend 都是实现者自己的判断,0.4.14 的 trait 文档没有写内存序要求。
  • spin 0.9.9 的 lock_api feature 把自己的 Mutex<()> 当 raw 锁,lock 时 mem::forget 掉自己的守卫;它的自旋是 test-and-test-and-set,比文档例子少写缓存行。
  • 用原子计数测临界区重叠:正确的两把锁 0 次,故意坏掉的锁 7.6–8.0 万次(共 8 万次进入);这组反向验证说明测法能发现「不互斥」。
  • 外壳管不了互斥、内存序、标记选择、重入、等待策略和公平;最后两项正是自旋锁与 parking_lot 的分界。

💬 关联讨论

发起讨论
加载中...