raw trait 与契约
主线位置:主线 1 的入口。
Mutex::lock()第一步就是self.raw.lock(),MutexGuard析构时调self.mutex.raw.unlock();这里的raw是什么、它向外壳承诺了什么,由本章的 trait 规定。parking_lot 的RawMutex怎么兑现这些承诺见 RawMutex 的一字节状态机。
flowchart LR rm["RawMutex"] --> rmf["RawMutexFair:unlock_fair、bump"] rm --> rmt["RawMutexTimed:try_lock_for / until"] rw["RawRwLock"] --> rwf["RawRwLockFair"] rw --> rwd["RawRwLockDowngrade"] rw --> rwt["RawRwLockTimed"] rw --> rwr["RawRwLockRecursive"] rwr --> rwrt["RawRwLockRecursiveTimed"] rwt --> rwrt rw --> rwu["RawRwLockUpgrade"] rwu --> rwuf["RawRwLockUpgradeFair"] rwf --> rwuf rwu --> rwud["RawRwLockUpgradeDowngrade"] rwd --> rwud rwu --> rwut["RawRwLockUpgradeTimed"] rwt --> rwut gt["GetThreadId"] --> rr["RawReentrantMutex<R, G>(结构体,不是 trait)"] rm --> rr
箭头从父 trait 指向子 trait(子 trait 以父 trait 为 supertrait)。外壳类型按 raw 锁实现了哪些 trait 来决定暴露哪些方法:R: RawMutexFair 时 MutexGuard 才有 unlock_fair / bump,R: RawRwLockUpgrade 时 RwLock 才有 upgradable_read。没实现的 trait 对应的方法根本不存在,不是运行时报错。
RawMutex 的六个成员
lock_api/src/mutex.rs 第 27–79 行,去掉文档注释:
/// # Safety
///
/// Implementations of this trait must ensure that the mutex is actually
/// exclusive: a lock can't be acquired while the mutex is already locked.
pub unsafe trait RawMutex {
// A “non-constant” const item is a legacy way to supply an initialized value to downstream
// static items. Can hopefully be replaced with `const fn new() -> Self` at some point.
#[allow(clippy::declare_interior_mutable_const)]
const INIT: Self;
type GuardMarker;
fn lock(&self);
fn try_lock(&self) -> bool;
unsafe fn unlock(&self);
#[inline]
fn is_locked(&self) -> bool {
let acquired_lock = self.try_lock();
if acquired_lock {
// Safety: The lock has been successfully acquired above.
unsafe {
self.unlock();
}
}
!acquired_lock
}
}
| 成员 | 作用 | 谁依赖它 |
|---|---|---|
const INIT: Self | 未上锁时的值 | Mutex::new 是 const fn,靠它在常量上下文里造出 raw 锁(外壳与守卫) |
type GuardMarker | 守卫是否 Send | MutexGuard 的 PhantomData(GuardSend 与 send_guard) |
fn lock(&self) | 阻塞直到拿到锁 | Mutex::lock、lock_arc、unlocked 结束时重新加锁 |
fn try_lock(&self) -> bool | 不阻塞地试一次 | Mutex::try_lock、默认的 is_locked |
unsafe fn unlock(&self) | 解锁 | 所有守卫的 Drop、force_unlock |
fn is_locked(&self) | 查询状态,有默认实现 | Mutex::is_locked |
为什么 trait 是 unsafe
Mutex<R, T> 是安全类型:拿到守卫的代码可以通过 DerefMut 得到 &mut T,而守卫里只有一个 &Mutex。这个 &mut T 之所以不和别人的 &mut T 重叠,唯一的依据是 R::lock() 返回时「没有别人持有这把锁」。lock_api 无法检查这一点,它只能要求实现者承诺,于是 trait 声明成 unsafe trait,实现时必须写 unsafe impl:实现者签字担保「锁真的互斥」,外壳据此给出 &mut T。
如果实现者违约,后果出现在安全代码里:两个线程同时拿着同一个 T 的 &mut。给自己的锁套外壳 里的实验用一个 lock 什么都不等的「锁」演示了这一点,为了不真的制造数据竞争,那个实验只通过 raw API 计数临界区重叠,没有把坏锁装进 Mutex。
0.4.14 的契约原文只有上面那一句「must ensure that the mutex is actually exclusive」,没有写内存序。0.5.0(随 parking_lot 0.13)把它改写成了一段,开头是「Implementations must provide mutual exclusion, with acquire semantics on successful locking and release semantics on unlocking」,并补上了移动已上锁的锁、unwind 时的要求(延后唤醒与守卫状态)。
为什么 unlock 是 unsafe fn
trait 是 unsafe 约束的是实现者,unlock 是 unsafe fn 约束的是调用者。第 54–65 行 的 Safety 一节:
This method may only be called if the mutex is held in the current context, i.e. it must be paired with a successful call to
lock,try_lock,try_lock_forortry_lock_until.
「in the current context」不是「in the current thread」:守卫是 Send 时(spin,或开了 send_guard 的 parking_lot),锁可以在另一个线程上释放。parking_lot 自己的 RawMutex::unlock 不检查调用者是谁,一个没上锁的 mutex 上调 unlock 会走 unlock_slow,结果取决于当时的状态位,所以这个前提必须由调用方保证。
lock_api 自己调用 unlock 的地方都在守卫里,每处都带一行 // Safety: A MutexGuard always holds the lock.。对用户开放的 unsafe 入口有三个:Mutex::force_unlock(第 252–267 行,要求「当前线程逻辑上持有一个被 mem::forget 掉的守卫」)、Mutex::raw()(拿到 &R 之后可以自己调 unlock)、make_guard_unchecked(凭空造守卫)。
unlock 以前不是 unsafe:0.11.0 / lock_api 0.4.0 的 CHANGELOG 写「Most methods on the Raw* traits have been made unsafe since they assume the current thread holds the lock. (#243)」,对应提交 14e28ca「lock_api: Make RawMutex::unlock() unsafe.」(2020-06)。
INIT 而不是 const fn new
stable Rust 的 trait 里不能写 const fn,所以「怎么造一把未上锁的锁」只能用关联常量表达。Mutex::new 是 const fn(第 146–161 行),里面写 raw: R::INIT,这样 static M: Mutex<u32> = Mutex::new(0); 就能成立。注释说这是「legacy way」,希望将来换成 const fn new() -> Self;0.5.0 里这个常量仍然在。
INIT 的类型是 Self,每次使用都会复制出一个新值,clippy 的 declare_interior_mutable_const 会对含原子类型的常量报警,所以这里显式 allow。
is_locked 的默认实现
默认实现是「试着锁一下,锁上了就立刻放掉」。它对任何实现都正确,但有副作用:一次成功的 try_lock + unlock 对别的线程是可见的一次加锁,parking_lot 里会走 unlock 的完整路径。parking_lot 的三个 raw 锁都覆盖了它,只读一次状态字(raw_mutex.rs 第 113–116 行:state.load(Relaxed) & LOCKED_BIT != 0)。0.13 把 is_locked 改成了必须实现的方法(#533 中的 bb2d12a「Require accurate lock state queries」)。
RawMutexFair 与 RawMutexTimed
pub unsafe trait RawMutexFair: RawMutex {
unsafe fn unlock_fair(&self);
unsafe fn bump(&self) {
self.unlock_fair();
self.lock();
}
}
pub unsafe trait RawMutexTimed: RawMutex {
type Duration;
type Instant;
fn try_lock_for(&self, timeout: Self::Duration) -> bool;
fn try_lock_until(&self, timeout: Self::Instant) -> bool;
}
unlock_fair的定义在 trait 文档里:「a lock is handed directly over to the next waiting thread if there is one, without giving other threads the opportunity to "steal" the lock in the meantime」。bump的默认实现就是先公平解锁再加锁;文档说实现者可以在「没有等待者」时做得更便宜。parking_lot 的实现只在状态字有PARKED_BIT时才走慢路径(公平解锁、bump 与 FairMutex)。RawMutexTimed不直接用std::time::Duration:lock_api 是no_std,时间类型由实现者指定。parking_lot 指定的是std::time::Duration/Instant(raw_mutex.rs第 141–144 行)。
RwLock 的 10 个 trait
lock_api/src/rwlock.rs 开头 316 行全是 trait。RawRwLock 本身(第 27–99 行)的契约:
Implementations of this trait must ensure that the
RwLockis actually exclusive: an exclusive lock can't be acquired while an exclusive or shared lock exists, and a shared lock can't be acquire while an exclusive lock exists.
成员和 RawMutex 一一对应,多一组 shared 操作和一个 is_locked_exclusive(默认实现是 try_lock_shared 再放掉)。扩展 trait:
| trait | 父 trait | 增加的方法 | parking_lot | spin 0.9.9 |
|---|---|---|---|---|
RawRwLock | — | lock_shared / try_lock_shared / unlock_shared、lock_exclusive / try_lock_exclusive / unlock_exclusive、is_locked、is_locked_exclusive | 是 | 是 |
RawRwLockFair | RawRwLock | unlock_shared_fair、unlock_exclusive_fair、bump_shared、bump_exclusive | 是 | 否 |
RawRwLockDowngrade | RawRwLock | downgrade(写 → 读) | 是 | 是 |
RawRwLockTimed | RawRwLock | try_lock_shared_for / until、try_lock_exclusive_for / until | 是 | 否 |
RawRwLockRecursive | RawRwLock | lock_shared_recursive、try_lock_shared_recursive | 是 | 否 |
RawRwLockRecursiveTimed | Recursive + Timed | try_lock_shared_recursive_for / until | 是 | 否 |
RawRwLockUpgrade | RawRwLock | lock_upgradable / try_lock_upgradable / unlock_upgradable、upgrade、try_upgrade | 是 | 是 |
RawRwLockUpgradeFair | Upgrade + Fair | unlock_upgradable_fair、bump_upgradable | 是 | 否 |
RawRwLockUpgradeDowngrade | Upgrade + Downgrade | downgrade_upgradable、downgrade_to_upgradable | 是 | 否(见下) |
RawRwLockUpgradeTimed | Upgrade + Timed | try_lock_upgradable_for / until、try_upgrade_for / until | 是 | 否 |
parking_lot 一列来自 src/raw_rwlock.rs 里的 10 个 unsafe impl(第 59、154、189、203、268、292、320、396、427、452 行)。spin 一列来自 registry 里 spin-0.9.9/src/rwlock.rs:RawRwLock、RawRwLockUpgrade、RawRwLockDowngrade 三个在 #[cfg(feature = "lock_api")] 下实现;RawRwLockUpgradeDowngrade 的实现写在 #[cfg(feature = "lock_api1")] 下,而 spin 0.9.9 的 Cargo.toml 没有名为 lock_api1 的 feature,所以这段代码不会被编译。
文档里有两条和语义直接相关的约束:
RawRwLockRecursive(第 187–200 行):「guaranteed to succeed without blocking if another read lock is held at the time of the call」,代价是「can cause writers to starve」。parking_lot 怎么做到见 读、写与写者优先。0.13 删掉了这两个 trait,改成单独的读者优先类型(去掉的与修正的)。RawRwLockUpgrade(第 213–247 行):「There may only be one upgradable lock at any time, otherwise deadlocks could occur when upgrading」。可升级读者同一时刻只能有一个,这是 trait 契约的一部分,不是 parking_lot 的实现选择(可升级读、降级与 with_upgraded)。
GetThreadId 与 RawReentrantMutex
可重入锁要知道「现在是谁在拿锁」,lock_api 把这件事也抽成了 trait(remutex.rs 第 35–55 行):
/// Implementations of this trait must ensure that no two active threads share
/// the same thread ID. However the ID of a thread that has exited can be
/// re-used since that thread is no longer active.
pub unsafe trait GetThreadId {
const INIT: Self;
fn nonzero_thread_id(&self) -> NonZeroUsize;
}
RawReentrantMutex<R, G>(第 57–69 行)是一个结构体,不是 trait:它把任意 R: RawMutex 加上 owner: AtomicUsize、lock_count: Cell<usize> 包成可重入的。文档特别说明它「has the same methods as the RawMutex trait, it does not implement it」:一个能被同一线程重复加锁的东西如果实现了 RawMutex,就能被装进 Mutex<R, T>,同一线程拿到两个 &mut T。parking_lot 的 RawThreadId 取一个线程局部变量的地址当 id,细节见 ReentrantMutex。
no_std 与 atomic_usize
lib.rs 第 88 行 是 #![no_std];只有 arc_lock feature 会引入 alloc(Arc 守卫要用)。trait 和外壳本身不分配内存,也不知道线程是什么,所以裸机、内核里的自旋锁也能用它。
可重入锁要一个 AtomicUsize 存 owner,有的目标平台没有原子 usize。第 111–114 行 把整个 remutex 模块放在默认 feature atomic_usize 后面:
#[cfg(feature = "atomic_usize")]
mod remutex;
#[cfg(feature = "atomic_usize")]
pub use crate::remutex::*;
这个 feature 是 lock_api 0.4.10 加的,CHANGELOG 原文「Add atomic_usize default feature to support platforms without atomics. (#380)」,提交 e95883e(2023-04)。关掉默认 feature 的用户就没有 ReentrantMutex;Mutex / RwLock 不受影响。
契约写在文档里,不在类型里
把这一章的 trait 放在一起看,类型系统只表达了「有哪些操作」,下面这些都只写在文档里:
| 要求 | 写在哪 | 违约的后果出现在 |
|---|---|---|
| 锁真的互斥 | RawMutex / RawRwLock 的 Safety | 安全代码里的 &mut T 重叠 |
unlock 只在持锁时调 | unlock 的 Safety | 实现者的状态机进入未定义的状态 |
| 可升级锁同一时刻只有一个 | RawRwLockUpgrade 的文档 | 两个 upgrade 互等 |
| 线程 id 互不相同 | GetThreadId 的 Safety | 两个线程同时进入可重入锁 |
| 递归读不阻塞 | RawRwLockRecursive 的文档 | 同线程重复读死锁 |
0.13 的 lock_api 0.5.0 用类型补了一部分:lock 返回一个代表「这一次加锁」的值,unlock 必须把它交回去(#544),见 延后唤醒与守卫状态。
小结
RawMutex是unsafe trait:实现者担保互斥,外壳据此在安全代码里给出&mut T;unlock是unsafe fn:调用者担保持有锁,「in the current context」不等于「in the current thread」。INIT关联常量让Mutex::new成为const fn;is_locked的默认实现是try_lock+unlock,parking_lot 覆盖成一次load,0.13 改成必须实现。- RwLock 一共 10 个 trait(不是 9 个),parking_lot 全部实现,spin 0.9.9 实现 3 个;
RawRwLockUpgradeDowngrade在 spin 里写在一个不存在的 feature 后面。 RawReentrantMutex故意不实现RawMutex;整个可重入模块在默认 featureatomic_usize后面。- 0.4.14 的契约原文没有写内存序,0.5.0 补写了 acquire / release 语义和 unwind 要求。