Rust in Everything
基于 parking_lot 0.12.5

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 里,与这一页无关(见 外壳与守卫)。这一页只讲那一个字节。

两位,四种状态

raw_mutex.rs:25-55:

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

raw_mutex.rs:65-110:

#[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);
    }
}

逐段看:

  1. 抢:只看 LOCKED_BIT。注释写得很直白:「即使有人在排队」也抢。这就是插队(barging):一个刚到的线程可以在排了很久的线程前面拿到锁。好处是不必每次交接都经过一次唤醒(唤醒要一次系统调用加一次调度),代价是不公平,0.12.5 用「最终公平」把最坏情况兜在约 1 ms(公平解锁、bump 与 FairMutex)。

  2. 转:只有在没人排队(PARKED_BIT 为 0)时才自旋。SpinWait::spin 前 3 次各转 2、4、8 次 spin_loop,接下来 7 次 yield_now,第 11 次返回 false(SpinWait)。一旦有人在停车,新来的线程不再转,直接去停车:既然已经有人等得要睡了,锁大概不会马上空出来。

  3. 置位:CAS 把 PARKED_BIT 加上。这里用 Relaxed,因为这个位的作用只是让解锁方走慢路径,真正决定「睡还是不睡」的是下一步在桶锁里的 validate。

  4. 停车: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:回调里决定交接还是放开

raw_mutex.rs:291-323:

#[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,否则改成 01TOKEN_HANDOFF醒来即持有锁,直接返回
叫醒了一个,不公平还有人等写 10,否则写 00TOKEN_NORMAL醒来回到 lock_slow 第 1 步重抢
一个也没找到(停车者恰好都超时走了)00TOKEN_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 时守卫照常解锁。

💬 关联讨论

发起讨论
加载中...