Rust in Everything
基于 parking_lot_core 0.9.12

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

thread_parker/mod.rs:1-51:

/// 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.rsfutextrue
其余 unix(本机 macOS)unix.rspthread 互斥量 + 条件变量false
windowswindows/mod.rsWaitOnAddress,或 NT keyed eventtrue
target_os = "redox"redox.rsredox 的 futex 系统调用true
fortanix SGXsgx.rsSGX usercall 的 wait / sendtrue
nightly feature + wasm + atomicswasm_atomic.rsmemory.atomic.wait32true
其余 wasmwasm.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 活多久」:

  1. 叫醒方在桶锁里 unpark_lock:锁住目标线程的 pthread mutex,返回句柄。
  2. 放开桶锁。
  3. 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 in std because 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::lockRawMutex::lock_slow → _pthread_cond_wait → __psynch_cvwait
卡在 std::thread::parkstd::thread::park → _dispatch_semaphore_wait_slow → _dispatch_sema4_wait → semaphore_wait_trap
卡在 std::sync::Mutex::lockstd::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_one2084、1459、13756084、7125、73334291、3125、2833每轮 1 / 22 000
std thread::park1166、1167、11673750、5917、51252375、2458、24585–9 / 22 000
crossbeam Parker1375、1458、13335667、5833、42912792、3125、26677–14 / 22 000
忙等(两个原子标志,下限)125、125、84458、375、125291、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。

💬 关联讨论

发起讨论
加载中...