RawMutex 的一字节状态机
主线位置:主线 1「一次有竞争的 lock / unlock」的锁这一侧。B 的
lock快路径 CAS 失败 →lock_slow:能抢就抢、没人排队就自旋、置PARKED_BIT、park(addr, validate);A 的unlock快路径 CAS 失败 →unlock_slow→unpark_one(addr, callback),回调在桶锁里决定是把锁直接交给 B(TOKEN_HANDOFF)还是放开让大家重新抢(TOKEN_NORMAL)。停车场那一侧见 park 与四种 unpark。
stateDiagram-v2 direction LR s00: 00 空闲 s01: 01 被持有,无人等 s11: 11 被持有,有人在停车或将要停车 s10: 10 已放开,等待者正被叫醒 s00 --> s01: lock 快路径 CAS 0 到 01 s01 --> s00: unlock 快路径 CAS 01 到 0 s01 --> s11: lock_slow 置 PARKED_BIT 后 park s11 --> s10: unlock_slow 回调,还有等待者,TOKEN_NORMAL s11 --> s00: unlock_slow 回调,没有等待者了 s11 --> s01: 公平交接,最后一个等待者,TOKEN_HANDOFF s11 --> s11: 公平交接,还有等待者 s10 --> s11: 任何线程抢到锁,包括刚来的
整个文件 raw_mutex.rs 331 行,parking_lot::Mutex<T> 是它的类型别名:mutex.rs:86 pub type Mutex<T> = lock_api::Mutex<RawMutex, T>;。守卫、map、try_lock_for 这些 API 都在 lock_api 里,与这一页无关(见 外壳与守卫)。这一页只讲那一个字节。
两位,四种状态
const LOCKED_BIT: u8 = 0b01;
const PARKED_BIT: u8 = 0b10;
pub struct RawMutex {
/// # State table:
///
/// PARKED_BIT | LOCKED_BIT | Description
/// 0 | 0 | The mutex is not locked, nor is anyone waiting for it.
/// 0 | 1 | The mutex is locked by exactly one thread. No other thread is
/// | | waiting for it.
/// 1 | 0 | The mutex is not locked. One or more thread is parked or about to
/// | | park. At least one of the parked threads are just about to be
/// | | unparked, or a thread heading for parking might abort the park.
/// 1 | 1 | The mutex is locked by exactly one thread. One or more thread is
/// | | parked or about to park, waiting for the lock to become available.
/// | | In this state, PARKED_BIT is only ever cleared when a bucket lock
/// | | is held (i.e. in a parking_lot_core callback). This ensures that
/// | | we never end up in a situation where there are parked threads but
/// | | PARKED_BIT is not set (which would result in those threads
/// | | potentially never getting woken up).
state: AtomicU8,
}
(分隔线省略。)这张表最后一格是整个文件的不变量:锁被持有时,PARKED_BIT 只能在桶锁里被清掉。原因在停车那一步:B 置了 PARKED_BIT 之后、真正挂进桶的队列之前,有一段窗口。如果 A 在这段窗口里不拿桶锁就把 PARKED_BIT 清掉并离开,B 随后睡下,就再也没有人知道要叫醒它。parking_lot_core 的协议是 B 在桶锁里跑 validate 再入队,A 在桶锁里跑回调改状态,两边都在同一把桶锁下,先后顺序就定死了。
10(没锁、但有等待者)是一个过渡态:解锁方已经放开了锁、正在叫醒一个停车者,或者某个打算停车的线程在 validate 失败后会回头重试。这时任何线程都可以抢锁,抢到就变成 11。
状态只占一个 u8,RawMutex 的大小和对齐都是 1(E1,见 和 std Mutex 比)。等待队列不在锁里,而在停车场那张全局哈希表的桶里(停车场哈希表)。
快路径:各一次 CAS
#[inline]
fn lock(&self) {
if self
.state
.compare_exchange_weak(0, LOCKED_BIT, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
self.lock_slow(None);
}
unsafe { deadlock::acquire_resource(self as *const _ as usize) };
}
#[inline]
unsafe fn unlock(&self) {
deadlock::release_resource(self as *const _ as usize);
if self
.state
.compare_exchange(LOCKED_BIT, 0, Ordering::Release, Ordering::Relaxed)
.is_ok()
{
return;
}
self.unlock_slow(false);
}
lock只在状态恰好是0时成功;10(没锁但有人排队)也会进慢路径,在慢路径里再抢。unlock只在状态恰好是01时成功;只要PARKED_BIT置着就进慢路径去叫人。deadlock::acquire_resource/release_resource在没开deadlock_detection时是空的内联函数(死锁检测)。开了以后每次加解锁都多一次线程局部记账,所以本卷的计时实验都在不开这个 feature 的 crate 里做。- 内存序:加锁成功用
Acquire、解锁用Release,这是锁保护数据的那一对;失败分支都是Relaxed,因为失败时什么也没拿到。分类沿用 crossbeam 卷的内存序。
try_lock(78-97 行)不一样:它是一个 CAS 循环,只要 LOCKED_BIT 没置就把它置上,保留 PARKED_BIT。所以 try_lock 能从 10 抢到锁,和 lock_slow 一样插队。
is_locked(113-116 行)是一次 Relaxed 读,只能当提示用:读完的下一刻状态就可能变了。
lock_slow:抢、转、置位、停车
raw_mutex.rs:209-289,去掉注释后是一个四段的循环:
#[cold]
fn lock_slow(&self, timeout: Option<Instant>) -> bool {
let mut spinwait = SpinWait::new();
let mut state = self.state.load(Ordering::Relaxed);
loop {
// 1. Grab the lock if it isn't locked, even if there is a queue on it
if state & LOCKED_BIT == 0 {
match self.state.compare_exchange_weak(
state, state | LOCKED_BIT, Ordering::Acquire, Ordering::Relaxed) {
Ok(_) => return true,
Err(x) => state = x,
}
continue;
}
// 2. If there is no queue, try spinning a few times
if state & PARKED_BIT == 0 && spinwait.spin() {
state = self.state.load(Ordering::Relaxed);
continue;
}
// 3. Set the parked bit
if state & PARKED_BIT == 0 {
if let Err(x) = self.state.compare_exchange_weak(
state, state | PARKED_BIT, Ordering::Relaxed, Ordering::Relaxed) {
state = x;
continue;
}
}
// 4. Park our thread until we are woken up by an unlock
let addr = self as *const _ as usize;
let validate = || self.state.load(Ordering::Relaxed) == LOCKED_BIT | PARKED_BIT;
let before_sleep = || {};
let timed_out = |_, was_last_thread| {
if was_last_thread {
self.state.fetch_and(!PARKED_BIT, Ordering::Relaxed);
}
};
match unsafe { parking_lot_core::park(addr, validate, before_sleep, timed_out,
DEFAULT_PARK_TOKEN, timeout) } {
ParkResult::Unparked(TOKEN_HANDOFF) => return true,
ParkResult::Unparked(_) => (),
ParkResult::Invalid => (),
ParkResult::TimedOut => return false,
}
spinwait.reset();
state = self.state.load(Ordering::Relaxed);
}
}
逐段看:
抢:只看
LOCKED_BIT。注释写得很直白:「即使有人在排队」也抢。这就是插队(barging):一个刚到的线程可以在排了很久的线程前面拿到锁。好处是不必每次交接都经过一次唤醒(唤醒要一次系统调用加一次调度),代价是不公平,0.12.5 用「最终公平」把最坏情况兜在约 1 ms(公平解锁、bump 与 FairMutex)。转:只有在没人排队(
PARKED_BIT为 0)时才自旋。SpinWait::spin前 3 次各转 2、4、8 次spin_loop,接下来 7 次yield_now,第 11 次返回 false(SpinWait)。一旦有人在停车,新来的线程不再转,直接去停车:既然已经有人等得要睡了,锁大概不会马上空出来。置位:CAS 把
PARKED_BIT加上。这里用Relaxed,因为这个位的作用只是让解锁方走慢路径,真正决定「睡还是不睡」的是下一步在桶锁里的validate。停车:
park先锁住addr所在的桶,在桶锁里执行validate。validate要求状态恰好是11:- 如果此时锁已经被放开(
10或00),睡下去就没人叫了,返回Invalid,回到第 1 步重抢; - 如果
PARKED_BIT已经没了(只可能是别人在桶锁里清的,例如上一个停车者超时退出时was_last_thread),也重来,下一轮会重新置位。
validate通过才入队、放桶锁、睡。醒来后看令牌:TOKEN_HANDOFF表示锁已经直接交给了自己,返回;TOKEN_NORMAL表示「锁放开了,你去抢」,spinwait.reset()后回到第 1 步,这一次可能又被别人插队,那就再转、再睡。- 如果此时锁已经被放开(
timed_out 回调只在 try_lock_for / try_lock_until 带超时的路径上用到:超时的线程在桶锁里把自己摘下来,如果它是这把锁最后一个停车者,就顺手清掉 PARKED_BIT。这正是状态表里「只能在桶锁里清」的那种情况。超时细节(醒来后重新锁桶、用 parker.timed_out() 精确判断)在 park 与四种 unpark。
validate、timed_out 都在桶锁里执行,所以都只做一次原子读写,不 panic,也不调 parking_lot 的任何函数——这是 park 的 unsafe 契约(源码里 257-260 行的 SAFETY 注释逐条对应)。
unlock_slow:回调里决定交接还是放开
#[cold]
fn unlock_slow(&self, force_fair: bool) {
let addr = self as *const _ as usize;
let callback = |result: UnparkResult| {
// If we are using a fair unlock then we should keep the
// mutex locked and hand it off to the unparked thread.
if result.unparked_threads != 0 && (force_fair || result.be_fair) {
// Clear the parked bit if there are no more parked threads.
if !result.have_more_threads {
self.state.store(LOCKED_BIT, Ordering::Relaxed);
}
return TOKEN_HANDOFF;
}
// Clear the locked bit, and the parked bit as well if there
// are no more parked threads.
if result.have_more_threads {
self.state.store(PARKED_BIT, Ordering::Release);
} else {
self.state.store(0, Ordering::Release);
}
TOKEN_NORMAL
};
unsafe {
parking_lot_core::unpark_one(addr, callback);
}
}
unpark_one(core/src/parking_lot.rs:732)锁桶、找到第一个 key 等于 addr 的线程摘下来、看后面还有没有同 key 的线程(have_more_threads)、按桶的计时器算 be_fair,然后在桶锁里调这个回调。回调有三种出口:
| 情况 | 写回的状态 | 令牌 | 被叫醒的线程 |
|---|---|---|---|
叫醒了一个,且要公平(force_fair 或 be_fair) | 保持 LOCKED_BIT;还有人等就保持 11,否则改成 01 | TOKEN_HANDOFF | 醒来即持有锁,直接返回 |
| 叫醒了一个,不公平 | 还有人等写 10,否则写 00 | TOKEN_NORMAL | 醒来回到 lock_slow 第 1 步重抢 |
| 一个也没找到(停车者恰好都超时走了) | 00 | TOKEN_NORMAL(没人收) | — |
三点值得注意:
- 公平交接时状态里的
LOCKED_BIT从头到尾没清过,锁的所有权在桶锁里从 A 直接转给 B,别的线程看到的一直是「被持有」,插不了队。store(LOCKED_BIT, Relaxed)用Relaxed是因为没有发布任何东西:B 醒来时通过 parker 的唤醒建立了先后关系(unpark/park这一对,ThreadParker)。 - 不公平放开时用
Release写回:这一刻锁真正空出来,下一个抢到的线程用Acquire的 CAS 与它配对。 - 解锁方在整个
unpark_one期间(锁桶、扫队列、回调)锁一直是 LOCKED 的。 快路径失败到回调里写回状态之间,新来的线程看到的仍是11,只能停车(有人排队就不转)。这一段的长度取决于桶锁争用和队列长度。0.13 起解锁方先放锁再去叫人,这一点不同,见 延后唤醒与守卫状态。
be_fair 来自桶上的 FairTimeout:距上一次公平解锁超过一个 0–1 ms 的随机间隔就为真(eventual fairness)。它只在真的叫醒了线程时才计算,没人停车时根本不触发。
给 Condvar 用的两个函数
raw_mutex.rs:180-207:mark_parked_if_locked(锁着就加 PARKED_BIT 并返回 true,没锁返回 false)和 mark_parked(无条件 fetch_or(PARKED_BIT))。两个都是 pub(crate),注释写明「must be called while holding the queue lock」。Condvar::notify_* 把等待者从 condvar 的队列挪到 mutex 的队列时,在桶锁里用它们给 mutex 补上 PARKED_BIT,否则挪过去的线程永远不会被 unlock 叫醒(Condvar)。这也是 Condvar 只能配 parking_lot 自己的 RawMutex 的原因:它要直接改这个字节。
带超时的加锁
RawMutexTimed 的两个方法(141-178 行)先试一次快路径,失败就 lock_slow(Some(deadline))。try_lock_for(d) 用 util::to_deadline 把时长换成 Instant::now().checked_add(d),溢出时得到 None,等价于不设超时。超时在停车场里按绝对时间等;在 Apple 平台上 0.12.5 的 ThreadParker 用 gettimeofday 换算,见 ThreadParker 的各平台实现。
没有毒化
lock_api 的 MutexGuard::drop 只调 raw.unlock()(lock_api/src/mutex.rs:701-708),不检查线程是否正在 panic。所以持锁时 panic,unwind 会正常解锁,下一个线程拿到锁、看到的可能是改到一半的数据,且没有任何标志告诉它。std 的 Mutex 在这种情况下置毒化标志,之后的 lock() 返回 Err(PoisonError)。从 std 迁移过来时 .lock().unwrap() 变成 .lock(),这层检查也一起去掉了(坑 no-poison)。
Mutex::new 是 const fn,mutex.rs:91 另有一个 const_mutex 函数,是 const fn new 稳定之前留下的(lock_api 0.4.7 起 new 在 stable 上就是 const,CHANGELOG #325)。
小结
RawMutex= 一个AtomicU8的两位:LOCKED_BIT、PARKED_BIT。不变量:锁被持有时PARKED_BIT只在桶锁里清。- 快路径:
lock是一次0 → 01的 CAS(Acquire),unlock是一次01 → 0的 CAS(Release);try_lock保留PARKED_BIT。 lock_slow:能抢就抢(插队)→ 没人排队才自旋 → 置PARKED_BIT→park,validate要求状态恰好是11;醒来拿到TOKEN_HANDOFF就返回,否则重抢。unlock_slow:在桶锁里的回调决定公平交接(锁一直不放,TOKEN_HANDOFF)还是放开(写10或00,TOKEN_NORMAL);整个过程中锁保持 LOCKED。mark_parked_if_locked/mark_parked让Condvar的 requeue 能给 mutex 补上PARKED_BIT。- 没有毒化:panic 时守卫照常解锁。