Rust in Everything
基于 parking_lot_core 0.9.12

park 与四种 unpark

主线位置:主线 4 在这里集中讲,主线 1、2 在 core 里的那一半也在这里。park:取 ThreadData → 锁桶 → 桶锁里 validate → 入队、放桶锁 → before_sleep → 睡;超时醒来要重新锁桶、在锁里精确判断。unpark_*:锁桶 → 摘线程 → 桶锁里调回调、写 token、unpark_lock → 放桶锁 → unpark。

flowchart TB
  p0["park(key, validate, before_sleep, timed_out, park_token, timeout)"] --> p1["with_thread_data;lock_bucket(key)"]
  p1 --> p2{"桶锁里 validate()"}
  p2 -->|"false"| inv["放桶锁,返回 Invalid"]
  p2 -->|"true"| p3["写 key / park_token / parked_with_timeout,prepare_park,挂到链表尾,放桶锁"]
  p3 --> p4["before_sleep()(锁外)"]
  p4 --> p5{"有 timeout?"}
  p5 -->|"没有"| sleep["parker.park(),醒来即 Unparked(token)"]
  p5 -->|"有"| pu{"park_until 返回 true?"}
  pu -->|"是"| un["Unparked(token)"]
  pu -->|"否"| chk["lock_bucket_checked(&key):key 可能已被 requeue 改掉"]
  chk --> to{"桶锁里 parker.timed_out()?"}
  to -->|"否:刚好被叫醒"| un
  to -->|"是"| rm["自己出队,算 was_last_thread,调 timed_out(key, last),返回 TimedOut"]

park 的签名与契约

parking_lot.rs:562-598:

/// Parks the current thread in the queue associated with the given key.
///
/// The `validate` function is called while the queue is locked and can abort
/// the operation by returning false. If `validate` returns true then the
/// current thread is appended to the queue and the queue is unlocked.
///
/// The `before_sleep` function is called after the queue is unlocked but before
/// the thread is put to sleep. The thread will then sleep until it is unparked
/// or the given timeout is reached.
///
/// The `timed_out` function is also called while the queue is locked, but only
/// if the timeout was reached. It is passed the key of the queue it was in when
/// it timed out, which may be different from the original key if
/// `unpark_requeue` was called. It is also passed a bool which indicates
/// whether it was the last thread in the queue.
///
/// # Safety
///
/// You should only call this function with an address that you control, since
/// you could otherwise interfere with the operation of other synchronization
/// primitives.
///
/// The `validate` and `timed_out` functions are called while the queue is
/// locked and must not panic or call into any function in `parking_lot`.
///
/// The `before_sleep` function is called outside the queue lock and is allowed
/// to call `unpark_one`, `unpark_all`, `unpark_requeue` or `unpark_filter`, but
/// it is not allowed to call `park` or panic.
#[inline]
pub unsafe fn park(
    key: usize,
    validate: impl FnOnce() -> bool,
    before_sleep: impl FnOnce(),
    timed_out: impl FnOnce(usize, bool),
    park_token: ParkToken,
    timeout: Option<Instant>,
) -> ParkResult {
参数在哪执行用途例子
key—等待队列的名字,通常是同步原语的地址RawMutex 的地址;RawRwLock 还用 addr + 1 当第二条队列
validate桶锁里最后确认一次「真的要睡」,返回 false 就不睡RawMutex:状态仍是 `LOCKED
before_sleep入队、放桶锁之后,睡之前做那些「必须在入队之后才能做」的事Condvar:解锁 mutex
timed_out桶锁里,只在确实超时时收拾状态位;参数是超时时所在的 key 和「是不是这个 key 上最后一个线程」RawMutex:最后一个就清 PARKED_BIT
park_token—挂在线程上的一个 usize,给 unpark_filter 挑人用RawRwLock:读 / 写 / 可升级
timeout—绝对时间点try_lock_until

三种结果:Unparked(UnparkToken)(被叫醒,带着叫醒方给的 token)、Invalid(validate 返回了 false)、TimedOut。

unsafe 的三层意思:

  1. key 必须是你控制的地址。 停车场不知道 key 属于谁。拿别的同步原语的地址当 key 停车,会混进它的队列,被它的 unpark_one 当成自己人叫醒(或者让它找不到真正的等待者)。
  2. 桶锁里的回调不能 panic、不能调停车场。 validate、timed_out 以及各 unpark 函数的 callback 都在桶锁里执行。panic 会带着桶锁 unwind 出去,这把 WordLock 再也没人解;在回调里再调 park 或 unpark_* 会去锁桶,如果恰好是同一个桶就自锁(WordLock 不可重入)。
  3. before_sleep 可以 unpark,不能 park、不能 panic。 它在锁外,所以可以叫醒别人(Condvar 正是在这里解锁 mutex,可能触发 mutex 的 unpark_one);但此时本线程已经在队列里了,再 park 一次就是同一个 ThreadData 挂两次。

unpark_one 和 unpark_all 的文档还多一句:停车场的函数不可重入,在异步信号处理函数里调用可能破坏内部状态或死锁(parking_lot.rs:728-730)。

park:正常路径

parking_lot.rs:599-645:

    // Grab our thread data, this also ensures that the hash table exists
    with_thread_data(|thread_data| {
        // Lock the bucket for the given key
        let bucket = lock_bucket(key);

        // If the validation function fails, just return
        if !validate() {
            // SAFETY: We hold the lock here, as required
            bucket.mutex.unlock();
            return ParkResult::Invalid;
        }

        // Append our thread data to the queue and unlock the bucket
        thread_data.parked_with_timeout.set(timeout.is_some());
        thread_data.next_in_queue.set(ptr::null());
        thread_data.key.store(key, Ordering::Relaxed);
        thread_data.park_token.set(park_token);
        thread_data.parker.prepare_park();
        if !bucket.queue_head.get().is_null() {
            (*bucket.queue_tail.get()).next_in_queue.set(thread_data);
        } else {
            bucket.queue_head.set(thread_data);
        }
        bucket.queue_tail.set(thread_data);
        // SAFETY: We hold the lock here, as required
        bucket.mutex.unlock();

        // Invoke the pre-sleep callback
        before_sleep();

        // Park our thread and determine whether we were woken up by an unpark
        // or by our timeout. Note that this isn't precise: we can still be
        // unparked since we are still in the queue.
        let unparked = match timeout {
            Some(timeout) => thread_data.parker.park_until(timeout),
            None => {
                thread_data.parker.park();
                // call deadlock detection on_unpark hook
                deadlock::on_unpark(thread_data);
                true
            }
        };

        // If we were unparked, return now
        if unparked {
            return ParkResult::Unparked(thread_data.unpark_token.get());
        }

validate 是不丢唤醒的关键。 解锁方要叫醒谁,必须先锁同一个桶;等待方「检查条件」和「挂进链表」在同一次持桶锁期间完成。于是只有两种先后:解锁方先拿到桶锁——那它已经改完了状态,等待方随后的 validate 会看到并放弃;或者等待方先挂好——那解锁方一定能在链表里找到它。futex 用「内核里比较一个整数」做同一件事;这里换成了用户态的桶锁加任意闭包。rayon 卷的 sleep 模块 是同一个思路:睡前先拿每线程的 is_blocked 锁再登记,叫醒方也要拿这把锁。区别在于 rayon 每个 worker 一把锁,这里是全局哈希表里共享的桶锁。

ThreadData 的字段大多是 Cell,key 是 AtomicUsize 但用 Relaxed:这些字段只在两种时候被访问——本线程不在队列里时(只有自己碰)、或在桶锁里(别人碰)。桶锁的 Acquire / Release 给出了先后,不需要字段自己带内存序。key 之所以是原子的,是因为超时路径要在没有桶锁时读它(lock_bucket_checked 锁前那一次读),而 requeue 可能同时在别的桶锁里改它。

被叫醒后读 unpark_token:叫醒方在桶锁里写、然后 unpark,被叫醒方从 parker.park() 返回后读。两者之间的先后由 parker 提供(macOS 上是 pthread mutex 的加解锁,Linux 上是 futex 字的 Release 存 / Acquire 读,见 ThreadParker)。

park:超时路径

park_until 返回 false 只说明「时间到了」,不说明「没被叫醒」:线程还挂在链表里,叫醒方可能正好在这一刻摘下了它。所以要重新锁桶、在锁里精确判断(parking_lot.rs:647-704):

        // Lock our bucket again. Note that the hashtable may have been rehashed in
        // the meantime. Our key may also have changed if we were requeued.
        let (key, bucket) = lock_bucket_checked(&thread_data.key);

        // Now we need to check again if we were unparked or timed out. Unlike the
        // last check this is precise because we hold the bucket lock.
        if !thread_data.parker.timed_out() {
            // SAFETY: We hold the lock here, as required
            bucket.mutex.unlock();
            return ParkResult::Unparked(thread_data.unpark_token.get());
        }

        // We timed out, so we now need to remove our thread from the queue
        // ...(在链表里找到自己、摘掉,同时看前后还有没有同 key 的线程)
                timed_out(key, was_last_thread);
        // ...
        bucket.mutex.unlock();
        ParkResult::TimedOut
  • 这次锁的桶不一定是原来那个:表可能扩过(lock_bucket_checked 用当前表),key 可能被 unpark_requeue 改过(它锁上桶后再确认 key 没变)。
  • parker.timed_out() 是精确的:叫醒方摘线程和 unpark_lock 都在桶锁里完成,现在我们持着桶锁,要么它已经做完(timed_out() 为假,按被叫醒处理),要么还没开始(我们还在链表里,自己摘掉)。
  • timed_out(key, was_last_thread) 传的是当前 key。Condvar 用它判断「我是不是已经被挪到 mutex 上了」(requeued = k != addr),被挪过就不算超时,因为它马上要去 mutex 上排队。
  • 超时的线程不调 deadlock::on_unpark,死锁检测也忽略 parked_with_timeout 的线程(死锁检测)。

四种 unpark

函数摘几个回调返回谁用
unpark_one(key, callback)第一个同 key 的callback(UnparkResult) -> UnparkToken,桶锁里;没找到线程也会调UnparkResultRawMutex::unlock_slow、RawRwLock::unlock_shared_slow(addr + 1)
unpark_all(key, token)全部同 key 的没有,所有人拿同一个 token叫醒的个数Once(完成或 panic 后叫醒所有等待者)
unpark_requeue(from, to, validate, callback)由 validate 返回的 RequeueOp 决定validate() -> RequeueOp、callback(op, result) -> UnparkToken,都在两把桶锁里UnparkResultCondvar::notify_one / notify_all
unpark_filter(key, filter, callback)filter(ParkToken) -> FilterOp 逐个决定filter 与 callback 都在桶锁里UnparkResultRawRwLock::wake_parked_threads

unpark_one

    // Lock the bucket for the given key
    let bucket = lock_bucket(key);

    // Find a thread with a matching key and remove it from the queue
    // ...
    while !current.is_null() {
        if (*current).key.load(Ordering::Relaxed) == key {
            // Remove the thread from the queue
            // ...(摘掉;不是队尾就继续往后扫,看还有没有同 key 的,置 have_more_threads)

            // Invoke the callback before waking up the thread
            result.unparked_threads = 1;
            result.be_fair = (*bucket.fair_timeout.get()).should_timeout();
            let token = callback(result);

            // Set the token for the target thread
            (*current).unpark_token.set(token);

            // This is a bit tricky: we first lock the ThreadParker to prevent
            // the thread from exiting and freeing its ThreadData if its wait
            // times out. Then we unlock the queue since we don't want to keep
            // the queue locked while we perform a system call. Finally we wake
            // up the parked thread.
            let handle = (*current).parker.unpark_lock();
            // SAFETY: We hold the lock here, as required
            bucket.mutex.unlock();
            handle.unpark();

            return result;
        }
        // ...
    }

    // No threads with a matching key were found in the bucket
    callback(result);
    // SAFETY: We hold the lock here, as required
    bucket.mutex.unlock();
    result

两点:

  • 没找到线程也调 callback,此时 unparked_threads = 0。RawMutex 依赖这一点:PARKED_BIT 置上了但等待者还没挂进来(它在 validate 之前),或者它刚超时离开,解锁方仍然要在桶锁里把状态清干净。
  • 「这里有点绕」那段注释说的就是主线 4 的第二个问题:被叫醒的线程如果是带超时等待的,它可能正好超时醒来、发现自己被摘了、返回、线程退出、ThreadData 析构。叫醒方必须在持桶锁时先 unpark_lock,让对方无法在叫醒完成前离开;系统调用(unpark)则放在桶锁外面,免得桶锁被一次系统调用占着。各平台的 unpark_lock 怎么做到「让对方离不开」,见 ThreadParker:macOS 上锁住对方 parker 的 pthread mutex(对方的 timed_out() 要拿这把锁),Linux 上只是把 futex 字清零,之后的 FUTEX_WAKE 即使打到已释放的内存,注释说也只会返回 EFAULT。

unpark_all

摘下所有同 key 的线程,每个写同一个 token、unpark_lock,句柄先存进 SmallVec<[_; 8]>(8 个以内不分配堆内存),放桶锁后再逐个 unpark。没有回调,返回叫醒了几个。

unpark_requeue 与 RequeueOp

parking_lot.rs:516-533:

RequeueOp叫醒挪到 key_toCondvar 何时选它
Abort00state 已经不是这把 mutex(等待者都走了,或换了 mutex)
UnparkOneRequeueRest第一个其余全部notify_all,mutex 没被锁
RequeueAll0全部notify_all,mutex 被锁着
UnparkOne第一个0notify_one,mutex 没被锁
RequeueOne0第一个notify_one,mutex 被锁着

lock_bucket_pair 先锁两个桶(下标小的先锁),在两把锁里调 validate;摘下的线程要么记为待叫醒,要么把 key 改成 key_to、串成一段,最后整段接到 key_to 桶的链表尾。被挪走的线程完全不知道这件事:它仍然睡在自己的 parker 上,只是以后叫醒它的会是 key_to 的 unpark_one。全过程见 一次 notify_all。

unpark_filter 与 FilterOp

从队头开始,对每个同 key 的线程调 filter(park_token):

FilterOp含义
Unpark摘下,等会儿叫醒,继续扫
Skip留着,继续扫
Stop留着,停止扫描

扫完后在桶锁里调一次 callback,它返回的同一个 token 写给所有被摘下的线程,放桶锁后统一 unpark。RawRwLock 解写锁时用它一次叫醒「队列前面所有的读者 + 至多一个写者」:读者的 ParkToken 是 TOKEN_SHARED,写者是 TOKEN_EXCLUSIVE,filter 累加新状态,遇到装不下的就 Stop(读、写与写者优先)。这个函数随 RwLock 的 task-fair 策略一起加入(79af298,2016-08)。

UnparkResult 与两种 token

parking_lot.rs:494-514:

字段含义注意
unparked_threads叫醒了几个
requeued_threads挪走了几个只有 unpark_requeue 会置
have_more_threads队列里是否还有同 key 的线程文档说「只在叫醒了线程时才可能为 true」;但 unpark_filter 遇到 Skip / Stop、unpark_requeue 选 RequeueOne 时,没叫醒任何人也会置 true;UnparkOneRequeueRest / RequeueAll 则从不置它(源队列已经清空)
be_fair这个桶的公平计时器是否到期只在叫醒了线程时计算,见 eventual fairness
_sealed: ()私有字段注释:以后加字段不算破坏性变更

ParkToken(pub usize) 和 UnparkToken(pub usize) 是两个 usize 新类型,默认值都是 0。方向相反:ParkToken 是等待方挂在自己身上、给 unpark_filter 看的(「我要读 / 写 / 可升级读」);UnparkToken 是叫醒方交给被叫醒者的(RawMutex 用 TOKEN_HANDOFF = 1 表示「锁已经是你的了」)。两者的取值完全由上层的同步原语约定,停车场只负责传递。

一个桶里混着不同的 key

桶链表里的线程 key 各不相同(不同的锁哈希到同一个桶),所以每个 unpark 函数都是从链表头线性扫描、比较 key,unpark_one 找到之后还要接着扫完剩下的部分来算 have_more_threads。代价是 O(桶里线程数);grow_hashtable 让桶数至少是线程数的 3 倍,平均很短。最坏情况是很多线程等在同一把锁上——比如几十个线程抢一把 Mutex——这时链表里几乎都是同一个 key,unpark_one 摘下第一个后只扫一个节点就能确定 have_more_threads。

小结

  • park 的契约:key 是你控制的地址;validate / timed_out / callback 在桶锁里,不能 panic、不能再调停车场;before_sleep 在锁外,可以 unpark、不能 park。
  • 不丢唤醒:等待方的「检查」和「入队」在同一次持桶锁期间完成,叫醒方必须拿同一把桶锁。
  • 超时:park_until 返回 false 不精确,要用 lock_bucket_checked 重新锁桶(key 可能被 requeue 改了),在锁里用 parker.timed_out() 精确判断,确实超时才自己出队。
  • 叫醒方:桶锁里写 token、unpark_lock,放桶锁后才 unpark;前者保证对方离不开,后者让系统调用不占桶锁。
  • 四种 unpark 各有用户:unpark_one(Mutex、RwLock 第二队列)、unpark_all(Once)、unpark_requeue(Condvar)、unpark_filter(RwLock)。
  • UnparkResult::have_more_threads 的文档说法比代码窄;ParkToken / UnparkToken 的取值由上层约定。

💬 关联讨论

发起讨论
加载中...