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 的签名与契约
/// 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 的三层意思:
- key 必须是你控制的地址。 停车场不知道 key 属于谁。拿别的同步原语的地址当 key 停车,会混进它的队列,被它的
unpark_one当成自己人叫醒(或者让它找不到真正的等待者)。 - 桶锁里的回调不能 panic、不能调停车场。
validate、timed_out以及各 unpark 函数的callback都在桶锁里执行。panic 会带着桶锁 unwind 出去,这把WordLock再也没人解;在回调里再调park或unpark_*会去锁桶,如果恰好是同一个桶就自锁(WordLock不可重入)。 before_sleep可以 unpark,不能 park、不能 panic。 它在锁外,所以可以叫醒别人(Condvar正是在这里解锁 mutex,可能触发 mutex 的unpark_one);但此时本线程已经在队列里了,再 park 一次就是同一个ThreadData挂两次。
unpark_one 和 unpark_all 的文档还多一句:停车场的函数不可重入,在异步信号处理函数里调用可能破坏内部状态或死锁(parking_lot.rs:728-730)。
park:正常路径
// 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,桶锁里;没找到线程也会调 | UnparkResult | RawMutex::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,都在两把桶锁里 | UnparkResult | Condvar::notify_one / notify_all |
unpark_filter(key, filter, callback) | filter(ParkToken) -> FilterOp 逐个决定 | filter 与 callback 都在桶锁里 | UnparkResult | RawRwLock::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
RequeueOp | 叫醒 | 挪到 key_to | Condvar 何时选它 |
|---|---|---|---|
Abort | 0 | 0 | state 已经不是这把 mutex(等待者都走了,或换了 mutex) |
UnparkOneRequeueRest | 第一个 | 其余全部 | notify_all,mutex 没被锁 |
RequeueAll | 0 | 全部 | notify_all,mutex 被锁着 |
UnparkOne | 第一个 | 0 | notify_one,mutex 没被锁 |
RequeueOne | 0 | 第一个 | 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
| 字段 | 含义 | 注意 |
|---|---|---|
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的取值由上层约定。