ThreadParker 的各平台实现
主线位置:主线 1 的最后一步(线程真正睡下、被叫醒),以及主线 4 的「
ThreadData活多久」。park入队、放桶锁后调parker.park();叫醒方在桶锁里unpark_lock(),放桶锁后unpark()。
flowchart TB
sel{"cfg_if:目标平台"} -->|"linux / android"| lin["linux.rs:AtomicI32 + FUTEX_WAIT / FUTEX_WAKE"]
sel -->|"其余 unix(本机 macOS)"| unix["unix.rs:Cell<bool> + pthread_mutex_t + pthread_cond_t"]
sel -->|"windows"| win["windows/:运行时选 WaitOnAddress 或 NT keyed event"]
sel -->|"redox / sgx / wasm 等"| other["各自的后端(一张表)"]
sel -->|"都不是"| gen["generic.rs:自旋忙等"]
两个 trait
/// Trait for the platform thread parker implementation.
///
/// All unsafe methods are unsafe because the Unix thread parker is based on
/// pthread mutexes and condvars. Those primitives must not be moved and used
/// from any other memory address than the one they were located at when they
/// were initialized. As such, it's UB to call any unsafe method on
/// `ThreadParkerT` if the implementing instance has moved since the last
/// call to any of the unsafe methods.
pub trait ThreadParkerT {
type UnparkHandle: UnparkHandleT;
const IS_CHEAP_TO_CONSTRUCT: bool;
fn new() -> Self;
/// Prepares the parker. This should be called before adding it to the queue.
unsafe fn prepare_park(&self);
/// Checks if the park timed out. This should be called while holding the
/// queue lock after `park_until` has returned false.
unsafe fn timed_out(&self) -> bool;
/// Parks the thread until it is unparked. This should be called after it has
/// been added to the queue, after unlocking the queue.
unsafe fn park(&self);
/// Parks the thread until it is unparked or the timeout is reached. ...
unsafe fn park_until(&self, timeout: Instant) -> bool;
/// Locks the parker to prevent the target thread from exiting. This is
/// necessary to ensure that thread-local `ThreadData` objects remain valid.
/// This should be called while holding the queue lock.
unsafe fn unpark_lock(&self) -> Self::UnparkHandle;
}
/// Handle for a thread that is about to be unparked. We need to mark the thread
/// as unparked while holding the queue lock, but we delay the actual unparking
/// until after the queue lock is released.
pub trait UnparkHandleT {
/// Wakes up the parked thread. This should be called after the queue lock is
/// released to avoid blocking the queue for too long.
unsafe fn unpark(self);
}
方法和调用时机一一对应:prepare_park 在入队前(桶锁里),park / park_until 在放桶锁之后,timed_out 在超时后重新拿到桶锁时,unpark_lock 在叫醒方的桶锁里,UnparkHandle::unpark 在叫醒方放开桶锁之后。方法都是 unsafe 的理由写在注释里:pthread 的互斥量和条件变量初始化后不能移动。IS_CHEAP_TO_CONSTRUCT 决定 WordLock 是把等待节点放在栈上还是线程局部存储里(WordLock)。
和 crossbeam 卷的 Parker 比:算法相同(一个「该不该睡」的标志,加一种能睡下的原语),差别在归属。crossbeam 的 Parker 是用户自己持有的对象,unpark 可以先于 park 发生、留下一个令牌;ThreadParker 嵌在停车场的 ThreadData 里,每线程一个,prepare_park 每次停车前把标志重置成「要睡」,队列和「要不要睡」的判断都由停车场在桶锁里做完了,parker 只负责「睡」和「醒」。
选哪个实现
thread_parker/mod.rs:53-85 按顺序匹配:
| 条件 | 文件 | 睡在什么上 | IS_CHEAP_TO_CONSTRUCT |
|---|---|---|---|
target_os = "linux" 或 "android" | linux.rs | futex | true |
其余 unix(本机 macOS) | unix.rs | pthread 互斥量 + 条件变量 | false |
windows | windows/mod.rs | WaitOnAddress,或 NT keyed event | true |
target_os = "redox" | redox.rs | redox 的 futex 系统调用 | true |
| fortanix SGX | sgx.rs | SGX usercall 的 wait / send | true |
nightly feature + wasm + atomics | wasm_atomic.rs | memory.atomic.wait32 | true |
| 其余 wasm | wasm.rs | 不能睡:所有方法直接 panic「Parking not supported on this platform」 | true |
| 都不是 | generic.rs | 自旋:while parked { spin_loop() } | true |
后四行只列表,不展开。值得一提的是两个极端:wasm.rs 一停车就 panic,README 第 89–95 行对应地说,wasm 上不开原子特性时「遇到死锁会 panic 而不是永远阻塞」;generic.rs 的 park 是纯忙等(generic.rs:43-59),一个线程睡多久就烧多久 CPU。0.13 起 generic 后端改为在等待时让出 CPU(#533 中的 4a3656f「Yield in generic thread parker」)。
macOS:pthread 互斥量 + 条件变量
本机走 unix.rs:
pub struct ThreadParker {
should_park: Cell<bool>,
mutex: UnsafeCell<libc::pthread_mutex_t>,
condvar: UnsafeCell<libc::pthread_cond_t>,
initialized: Cell<bool>,
}
impl super::ThreadParkerT for ThreadParker {
// ...
const IS_CHEAP_TO_CONSTRUCT: bool = false;
#[inline]
unsafe fn prepare_park(&self) {
self.should_park.set(true);
if !self.initialized.get() {
self.init();
self.initialized.set(true);
}
}
#[inline]
unsafe fn timed_out(&self) -> bool {
// We need to grab the mutex here because another thread may be
// concurrently executing UnparkHandle::unpark, which is done without
// holding the queue lock.
let r = libc::pthread_mutex_lock(self.mutex.get());
// ...
let should_park = self.should_park.get();
let r = libc::pthread_mutex_unlock(self.mutex.get());
// ...
should_park
}
#[inline]
unsafe fn park(&self) {
let r = libc::pthread_mutex_lock(self.mutex.get());
debug_assert_eq!(r, 0);
while self.should_park.get() {
let r = libc::pthread_cond_wait(self.condvar.get(), self.mutex.get());
debug_assert_eq!(r, 0);
}
let r = libc::pthread_mutex_unlock(self.mutex.get());
debug_assert_eq!(r, 0);
}
// ...
#[inline]
unsafe fn unpark_lock(&self) -> UnparkHandle {
let r = libc::pthread_mutex_lock(self.mutex.get());
debug_assert_eq!(r, 0);
UnparkHandle {
thread_parker: self,
}
}
}
以及叫醒(unix.rs:166-183):
impl super::UnparkHandleT for UnparkHandle {
#[inline]
unsafe fn unpark(self) {
(*self.thread_parker).should_park.set(false);
// We notify while holding the lock here to avoid races with the target
// thread. In particular, the thread could exit after we unlock the
// mutex, which would make the condvar access invalid memory.
let r = libc::pthread_cond_signal((*self.thread_parker).condvar.get());
debug_assert_eq!(r, 0);
let r = libc::pthread_mutex_unlock((*self.thread_parker).mutex.get());
debug_assert_eq!(r, 0);
}
}
should_park 是一个普通的 Cell<bool>,所有读写都在 parker 自己的 pthread mutex 里(prepare_park 例外:那时线程还没入队,没人会碰它),所以不需要原子类型。把三个函数串起来就是主线 4 的「ThreadData 活多久」:
- 叫醒方在桶锁里
unpark_lock:锁住目标线程的 pthread mutex,返回句柄。 - 放开桶锁。
unpark:在仍持着那把 pthread mutex 时置should_park = false、pthread_cond_signal,最后才解锁。
目标线程无论处在哪种状态,都要拿到同一把 pthread mutex 才能前进:睡在 pthread_cond_wait 里的,醒来时要重新拿锁;超时醒来的,要先重新锁桶(得等第 2 步),再调 timed_out()(得等第 3 步)。所以在叫醒方解锁 pthread mutex 之前,目标线程不可能返回、退出、析构 ThreadData。注释说的正是这一点:如果先解锁再 signal,目标线程可能在两步之间退出,signal 就打在已释放的条件变量上。
IS_CHEAP_TO_CONSTRUCT = false:pthread 对象在第一次 prepare_park 时才初始化(init),析构时要 pthread_mutex_destroy / pthread_cond_destroy,所以 WordLock 也把它放在线程局部存储里复用。
超时:墙上时钟
unix.rs:128-148 的 init 在 Linux 等平台上把条件变量设成 CLOCK_MONOTONIC,但在 Apple、Android、ESP-IDF 上是空函数——这些平台不支持 pthread_condattr_setclock。于是 Apple 上的超时要换算成墙上时钟(unix.rs:185-198):
// Returns the current time on the clock used by pthread_cond_t as a timespec.
#[cfg(target_vendor = "apple")]
#[inline]
fn timespec_now() -> libc::timespec {
let mut now = MaybeUninit::<libc::timeval>::uninit();
let r = unsafe { libc::gettimeofday(now.as_mut_ptr(), ptr::null_mut()) };
// ...
}
park_until(第 84–115 行)的循环每一圈先用单调时钟 Instant::now() 判断是否到期,没到期才把剩余时长加到 gettimeofday 的当前值上,算出绝对时间交给 pthread_cond_timedwait。由此可以从代码推出系统时间被调整时的行为(只读代码,本卷不改本机时间去实测):
- 系统时间往前拨:
pthread_cond_timedwait提前返回,但循环用单调时钟重新判断,没到期就按新的剩余时长再等一次,所以不会提前结束。 - 系统时间往回拨:已经交给内核的绝对时间点在墙上时钟里变远了,这一次等待会多等往回拨的那么久,直到被唤醒或那个绝对时间到达。
0.13 起不同:Apple 上改为相对时长的等待,不再受墙上时钟影响(#533 中的 c175220「Use relative timed waits on Apple platforms」),见 07 0.13 的变化。
Linux:futex
生产服务器走 linux.rs。本卷不在任何服务器上做实验,这一节只读代码。
pub struct ThreadParker {
futex: AtomicI32,
}
impl super::ThreadParkerT for ThreadParker {
// ...
const IS_CHEAP_TO_CONSTRUCT: bool = true;
#[inline]
unsafe fn prepare_park(&self) {
self.futex.store(1, Ordering::Relaxed);
}
#[inline]
unsafe fn timed_out(&self) -> bool {
self.futex.load(Ordering::Relaxed) != 0
}
#[inline]
unsafe fn park(&self) {
while self.futex.load(Ordering::Acquire) != 0 {
self.futex_wait(None);
}
}
// ...
#[inline]
unsafe fn unpark_lock(&self) -> UnparkHandle {
// We don't need to lock anything, just clear the state
self.futex.store(0, Ordering::Release);
UnparkHandle { futex: &self.futex }
}
}
impl super::UnparkHandleT for UnparkHandle {
#[inline]
unsafe fn unpark(self) {
// The thread data may have been freed at this point, but it doesn't
// matter since the syscall will just return EFAULT in that case.
let r = libc::syscall(
libc::SYS_futex,
self.futex,
libc::FUTEX_WAKE | libc::FUTEX_PRIVATE_FLAG,
1,
);
// ...
}
}
- 一个
AtomicI32:1 表示要睡,0 表示已被叫醒。futex_wait用FUTEX_WAIT | FUTEX_PRIVATE_FLAG、期望值 1,内核在睡下前再比较一次,值已经变成 0 就立即返回,所以unpark_lock的清零和对方的睡下之间没有丢唤醒的空隙。 unpark_lock不锁任何东西,只是在桶锁里store(0, Release);与park循环里的load(Acquire)配对,被叫醒的线程能看到叫醒方在桶锁里写的unpark_token。- 这和 macOS 的做法不同:清零之后,目标线程随时可能看到 0 并返回、退出。
unpark里的FUTEX_WAKE因此可能落在已释放的地址上,注释的说法是这种情况下系统调用只会返回EFAULT,debug_assert也允许这个错误码。 - 超时用相对时长(
FUTEX_WAIT的超时参数本来就是相对的),不受墙上时钟影响。 IS_CHEAP_TO_CONSTRUCT = true:构造就是一个整数,WordLock每次在栈上新建。
Windows:两种后端
windows/mod.rs:18-70:第一次构造 ThreadParker 时决定全局后端,先试 WaitOnAddress / WakeByAddressSingle(从 api-ms-win-core-synch-l1-2-0.dll 里 GetProcAddress,Windows 8 起才有),找不到再试 NT keyed event(ntdll.dll 的 NtCreateKeyedEvent 等,Windows XP 起有),都没有就 panic「parking_lot requires either NT Keyed Events (WinXP+) or WaitOnAddress/WakeByAddress (Win8+)」。注释说在构造时就选好,是为了「之后不会再 panic,以免把同步原语留在坏掉的状态」。README 第 5 条「Condvar、RwLock、Once 能在 Windows XP 上用」靠的就是 keyed event 这条退路。
keyed event 后端有一个和超时相关的细节:线程超时醒来时如果发现自己已经被 unpark_lock 标记为叫醒,必须再调一次 NtWaitForKeyedEvent 把那次 release 接住,否则叫醒方会卡在 NtReleaseKeyedEvent 里(keyed_event.rs 第 108–111、140–142 行的注释)。这和 macOS 上「叫醒方锁住对方」的作用相同:保证叫醒方的那次系统调用有人接。
thread_yield
SpinWait 后 7 步调的 thread_yield:除 Windows 外都是 std::thread::yield_now(),在 unix 上就是 sched_yield(rust-src sys/thread/unix.rs:781-784);Windows 上是 Sleep(0),注释说不用 SwitchToThread,因为它「不考虑系统里所有的线程,我们等的那个线程可能不会被选中」(windows/mod.rs:166-175)。
和 std 的 thread::park 对照
std 在 macOS 上的 thread::park 不用 pthread,而是 libdispatch 的信号量(rust-src sys/sync/thread_parking/darwin.rs:1-11):
Darwin actually has futex syscalls (
__ulock_wait/__ulock_wake), but they cannot be used instdbecause they are non-public (their use will lead to rejection from the App Store).
注释接着说信号量只要一个原语(不需要「互斥量 + 条件变量」一对),而底层的 Mach 信号量「是否公开存疑」,所以用 libdispatch 包的那一层。parking_lot_core 0.12.5 在 macOS 上用的是另一条路:每线程一对 pthread 对象。std 的 Mutex 和 Condvar 在 macOS 上也是 pthread(和 std Mutex 比)。
睡着时的栈顶
exp-parking_lot/park-stack:主线程持着一把 parking_lot::Mutex 和一把 std::sync::Mutex,三个线程分别卡在 parking_lot 的锁上、thread::park() 上、std 的锁上,用 sample <pid> 1 采样,三个线程的栈(去掉地址,从下往上):
| 线程 | 栈顶几帧 |
|---|---|
卡在 parking_lot::Mutex::lock | RawMutex::lock_slow → _pthread_cond_wait → __psynch_cvwait |
卡在 std::thread::park | std::thread::park → _dispatch_semaphore_wait_slow → _dispatch_sema4_wait → semaphore_wait_trap |
卡在 std::sync::Mutex::lock | std::sys::…::Mutex::lock → _pthread_mutex_firstfit_lock_slow → _pthread_mutex_firstfit_lock_wait → __psynch_mutexwait |
park、lock_bucket 等都被内联进了 lock_slow,栈上看不到停车场的函数名。所以在 macOS 上看到栈顶是 __psynch_cvwait 时,它可能是 std 的 Condvar::wait(rayon 卷:池内阻塞),也可能是卡在一把 parking_lot 的锁上,要看下面一帧;std 的锁自锁时则是 __psynch_mutexwait(tauri 卷:插件锁死锁)。
实验 E12:唤醒延迟
断言:macOS 上 parking_lot_core 的停车(pthread 条件变量)与 std thread::park(libdispatch 信号量)唤醒延迟同量级。
方法(exp-parking_lot/park-latency):两个线程乒乓。叫醒方把当前时间戳写进对方的槽、置标志、叫醒对方,然后自己等;被叫醒的一方醒来后立刻用「现在 − 时间戳」得到单向延迟,再叫醒回去。三种实现:parking_lot_core 的 park(validate 检查标志)/ unpark_one、std 的 thread::park / Thread::unpark、crossbeam 的 Parker / Unparker。每种 1000 次预热加 10 000 次往返,单向延迟两个方向合在一起取中位数和 p99,三轮。程序同时统计「没真正睡下就返回」的等待次数,确认测到的是睡眠唤醒。2026-11-01 22:23,开始时负载 1 分钟 2.59、5 分钟 2.92,不开 deadlock_detection。
| 实现 | 单向中位数(三轮,ns) | 单向 p99(三轮,ns) | 往返中位数(三轮,ns) | 没睡下的等待 |
|---|---|---|---|---|
parking_lot_core park / unpark_one | 2084、1459、1375 | 6084、7125、7333 | 4291、3125、2833 | 每轮 1 / 22 000 |
std thread::park | 1166、1167、1167 | 3750、5917、5125 | 2375、2458、2458 | 5–9 / 22 000 |
crossbeam Parker | 1375、1458、1333 | 5667、5833、4291 | 2792、3125、2667 | 7–14 / 22 000 |
| 忙等(两个原子标志,下限) | 125、125、84 | 458、375、125 | 291、292、250 | 全部(从不睡) |
- 三种真正睡下的实现都在 1.2–2.1 µs 之间,同一个量级,断言成立。中位数上 std 的信号量最快(约 1.17 µs),parking_lot_core(中位数的中位数 1.46 µs)与同样基于 pthread 条件变量的 crossbeam
Parker(1.38 µs)接近;parking_lot_core 每次还要多锁两次桶。p99 是中位数的 3–5 倍。 - 忙等的 84–125 ns 是「另一个核上的线程看到一次原子写」的时间,也就是这台机器上跨核通信的下限;睡眠唤醒比它贵一个数量级以上。这就是
SpinWait先自旋、再让出、最后才停车的理由(SpinWait)。
反向验证:一个线程先叫醒自己再等(parking_lot_core:unpark_one 找不到人,再 park 时 validate 直接返回 false;std 和 crossbeam:先 unpark 留下令牌,park 立刻返回),每 100 对计一次时,三轮中位数:parking_lot_core 10、12、10 ns,std 8 ns,crossbeam 8 ns。不睡下的路径只要十纳秒左右,比上表小两个数量级,说明上表的微秒量级确实来自睡眠和唤醒,而不是 API 本身的开销。parking_lot_core 这条路径包括两次 lock_bucket(哈希、WordLock 的 CAS 加解锁),仍然只有 10 ns 出头。
小结
ThreadParkerT五个方法对应停车场的五个时刻;unpark_lock在桶锁里,unpark在锁外。- macOS:
Cell<bool>+ pthread 互斥量 + 条件变量,叫醒方持着对方的 pthread mutex 完成 signal,保证对方在叫醒完成前不会退出;超时按gettimeofday换算,系统时间往回拨会多等,往前拨不会提前结束。 - Linux:一个
AtomicI32+ futex,unpark_lock只是 Release 清零,之后的FUTEX_WAKE可能打到已释放的地址,注释说只返回EFAULT。 - Windows 运行时选
WaitOnAddress或 keyed event;wasm 无原子时一停车就 panic;generic 后端忙等(0.13 改为让出)。 - std 在 macOS 上的
thread::park是 libdispatch 信号量;本机实测三种 park 的单向唤醒延迟都在 1.2–2.1 µs,忙等下限约 0.1 µs。 - 卡在 parking_lot 锁上的线程,macOS 栈顶是
lock_slow→__psynch_cvwait。