给自己的锁套外壳
主线位置:主线 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<T> = lock_api::Mutex<RawSpinlock, T>"] 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 行)先列三步:
- 定义 raw 锁类型,「This should only contain the lock state, not any data protected by the lock」。
- 为它实现
RawMutex。 - 把
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_exactly | 8 线程各 *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_sub | 0 次 |
raw_spin_crate_never_overlaps | 同上,raw 锁换成 spin::Mutex<()> | 0 次 |
broken_lock_is_detected(反向) | 同上,raw 锁换成 lock 什么都不等的 BrokenRawLock | 75 857 / 79 904 / 79 365 次(三次运行,共 80 000 次进入) |
guard_api_extras | map 到 Vec 的第一个元素再改写;映射守卫析构后 is_locked() 为假 | 通过 |
guard_is_send_with_guard_send | 编译期断言 SpinlockGuard<u64> 和 spin 的守卫是 Send | 通过;--features nosend 把标记换成 GuardNoSend 后编译失败(错误全文在 GuardSend 与 send_guard) |
sizes | size_of | Spinlock<()> 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 impl | CAS 保证;本章的重叠计数实验验证 |
| 内存序 | 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_apifeature 把自己的Mutex<()>当 raw 锁,lock时mem::forget掉自己的守卫;它的自旋是 test-and-test-and-set,比文档例子少写缓存行。 - 用原子计数测临界区重叠:正确的两把锁 0 次,故意坏掉的锁 7.6–8.0 万次(共 8 万次进入);这组反向验证说明测法能发现「不互斥」。
- 外壳管不了互斥、内存序、标记选择、重入、等待策略和公平;最后两项正是自旋锁与 parking_lot 的分界。