驱动栈与 park
主线位置:IO 主线的「worker 空闲时 park 在 IO 驱动上」和「mio 报告就绪 → wake」之间。线程在这里睡下,也在这里被叫醒。
flowchart TB
drv[Driver] --> td{TimeDriver}
td -->|enable_time| time[time::Driver]
td -->|未开| io1[IoStack]
time --> io1
io1 -->|enable_io| proc[process::Driver]
io1 -->|未开| pt["ParkThread:条件变量"]
proc --> sig[signal::Driver]
sig --> io[io::Driver]
io --> poll["mio Poll::poll"]
调度器没事做时要让线程睡下,等两类事件之一:IO 就绪、定时器到期。另外还要能被别的线程叫醒(有新任务进了队列)。tokio 把这些需求叠成一个栈,每层只认识自己下面那一层,调度器只看见最外层的 Driver。
一层套一层
driver.rs 里的 Driver 只有一个字段:
pub(crate) struct Driver {
inner: TimeDriver,
}
Driver::new 从里往外建:
pub(crate) fn new(cfg: Cfg) -> io::Result<(Self, Handle)> {
let (io_stack, io_handle, signal_handle) =
create_io_stack(cfg.enable_io, cfg.nevents, cfg.nevents_busy)?;
let clock = create_clock(cfg.enable_pause_time, cfg.start_paused);
let (time_driver, time_handle) =
create_time_driver(cfg.enable_time, cfg.timer_flavor, io_stack, &clock);
Ok((
Self { inner: time_driver },
Handle {
io: io_handle,
signal: signal_handle,
time: time_handle,
clock,
},
))
}
create_io_stack 决定最底下是什么:
let ret = if enabled {
let (io_driver, io_handle) = crate::runtime::io::Driver::new(nevents, nevents_busy)?;
let (signal_driver, signal_handle) = create_signal_driver(io_driver, &io_handle)?;
let process_driver = create_process_driver(signal_driver);
(IoStack::Enabled(process_driver), IoHandle::Enabled(io_handle), signal_handle)
} else {
let park_thread = ParkThread::new();
let unpark_thread = park_thread.unpark();
(IoStack::Disabled(park_thread), IoHandle::Disabled(unpark_thread), Default::default())
};
开了 IO,就是 IO 驱动外面包信号驱动,再包进程驱动;没开,就是一个 ParkThread。然后 create_time_driver 在外面再包一层时间驱动,或者不包:
pub(crate) enum TimeDriver {
Enabled {
driver: crate::runtime::time::Driver,
},
EnabledAlt(IoStack),
Disabled(IoStack),
}
EnabledAlt 是 unstable 的另一套定时器,它不在驱动栈里放时间驱动,定时器由 worker 在 park 前后自己处理(maintain_local_timers_before_parking),见 (unstable)time_alt。
每一层都是按值持有下一层,不是 Box<dyn Park>。层数和组合都是固定的,用枚举 match 分派,都是静态调用。
Park trait1.22.0 之前,层与层之间靠内部的 Park / Unpark 两个 trait 连接,比如时间驱动写成 Driver<P: Park>,对下一层泛型。2022 年 9 月的 #4991(1.22.0)把这两个 trait 删了。PR 说明给的来历:很多年前 tokio 拆成一组独立的 crate,那种结构需要一个 trait 来表示「把线程停下来,顺便干点活」;所有 crate 合成一个以后,这两个 trait 只剩下内部细节的作用。紧接着的 #5027(1.22.0)又删掉了各层自己的 Unpark 类型,改用 io::Handle、time::Handle 叫醒线程,也就是下面 driver::Handle 里的那几个字段。
#4991 还说,下一步打算把各个驱动解耦,放进一个结构体,不再嵌套。到 1.54.1,句柄那一半确实放进了一个结构体 driver::Handle,park 这一半仍然是本节的嵌套栈。
两次裁剪
驱动栈被裁剪两次:
| 时机 | 依据 | 例子 |
|---|---|---|
| 编译期 | feature flag | IO 相关的 feature(net,Unix 上的 process、signal,Linux 上 unstable 的 io-uring)一个都没开时,IoStack 直接是 struct IoStack(ParkThread),没有枚举;没开 time 时 TimeDriver 就是 IoStack 的别名;不是 Unix、或者没开 signal / process 时没有信号层,SignalDriver 就是 IoDriver;没开 process 时进程层同理 |
| 运行时 | enable_io / enable_time | IoStack::Disabled、TimeDriver::Disabled |
在开了全部 feature 的 Unix 上,运行时组合出来是四种:
enable_time | enable_io | 栈 | 最底层 |
|---|---|---|---|
| 开 | 开 | 时间 → 进程 → 信号 → IO | mio Poll |
| 开 | 关 | 时间 → ParkThread | 条件变量 |
| 关 | 开 | 进程 → 信号 → IO | mio Poll |
| 关 | 关 | ParkThread | 条件变量 |
信号和进程两层只在开了 IO 时存在:信号处理函数往一个 self-pipe 里写字节,signal::Driver 把管道的读端注册到 mio,靠 IO 驱动得知「有信号来了」;进程驱动要等 SIGCHLD,又依赖信号驱动。
共享的那一半:driver::Handle
Driver 只有一个,只有拿着它的线程能 park。其他线程要注册 IO、注册定时器、叫醒睡着的线程,用的是 driver::Handle,它放在调度器的 Handle 里,所有线程共享:
pub(crate) struct Handle {
/// IO driver handle
pub(crate) io: IoHandle,
/// Signal driver handle
#[cfg_attr(any(not(unix), loom), allow(dead_code))]
pub(crate) signal: SignalHandle,
/// Time driver handle
pub(crate) time: TimeHandle,
/// Source of `Instant::now()`
#[cfg_attr(not(all(feature = "time", feature = "test-util")), allow(dead_code))]
pub(crate) clock: Clock,
}
没开对应的驱动,取句柄时就 panic。io() 和 time() 里的 expect 消息:
A Tokio 1.x context was found, but IO is disabled. Call `enable_io` on the runtime builder to enable IO.
A Tokio 1.x context was found, but timers are disabled. Call `enable_time` on the runtime builder to enable timers.
「context was found」说明线程本地上下文里有运行时句柄,只是那个运行时没开这个驱动。和「there is no reactor running」是两种不同的错。
park:沿着栈往下走
最外层是时间驱动。time::Driver::park_internal 先算出最近一个定时器还有多久到期,把它作为下一层的超时:
fn park_internal(&mut self, rt_handle: &driver::Handle, limit: Option<Duration>) {
let handle = rt_handle.time();
let mut lock = handle.inner.lock();
assert!(!handle.is_shutdown());
let next_wake = lock.wheel.next_expiration_time();
lock.next_wake =
next_wake.map(|t| NonZeroU64::new(t).unwrap_or_else(|| NonZeroU64::new(1).unwrap()));
drop(lock);
match next_wake {
Some(when) => {
let now = handle.time_source.now(rt_handle.clock());
// Note that we effectively round up to 1ms here - this avoids
// very short-duration microsecond-resolution sleeps that the OS
// might treat as zero-length.
let mut duration = handle
.time_source
.tick_to_duration(when.saturating_sub(now));
if duration > Duration::from_millis(0) {
if let Some(limit) = limit {
duration = std::cmp::min(limit, duration);
}
self.park_thread_timeout(rt_handle, duration);
} else {
self.park.park_timeout(rt_handle, Duration::from_secs(0));
}
}
None => {
if let Some(duration) = limit {
self.park_thread_timeout(rt_handle, duration);
} else {
self.park.park(rt_handle);
}
}
}
// Process pending timers after waking up
handle.process(rt_handle.clock());
}
时间驱动自己从不睡,它只是把「最多睡多久」算出来交给下一层,醒来后处理到期的定时器。所以定时器的精度取决于下一层的超时等待:开了 IO 就是 epoll_wait / kevent 的超时,没开就是条件变量的 wait_timeout。时间轮以毫秒为刻度,时间轮本身见分层时间轮。
往下每一层都是同一个模式:先调下一层的 park,返回后做自己的事。process::Driver::park:
pub(crate) fn park(&mut self, handle: &driver::Handle) {
self.park.park(handle);
GlobalOrphanQueue::reap_orphans(&self.signal_handle);
}
pub(crate) fn park(&mut self, handle: &driver::Handle) {
self.io.park(handle);
self.process();
}
最底下的 io::Driver::park 调 turn,turn 里才有真正阻塞的那一句:
match self.poll.poll(events, max_wait) {
把整条链按时间顺序排一下:
| 层 | 往下调之前 | 往下调 | 返回之后 |
|---|---|---|---|
| 时间 | 查时间轮,算超时 | park_timeout(最近到期) 或 park | process:触发到期定时器的 waker |
| 进程 | — | park / park_timeout | reap_orphans:回收已退出的子进程 |
| 信号 | — | 同上 | process:若信号管道就绪,读空管道,广播信号 |
| IO | 释放已注销的 IO 资源 | Poll::poll(events, max_wait) | 逐个分发事件,叫醒等这个 IO 的任务 |
醒来后的处理是从里往外的:先分发 IO 事件,再广播信号,再回收子进程,最后触发定时器。turn 的细节见「反应器」。
没开 IO 时,最底层是 ParkThread::park / park_timeout,直接睡在条件变量上,下一节细看。
谁在调 park
调度器在两种情况下调驱动:
- 没事做,睡觉。 current_thread 的
Context::park调driver.park;多线程的 worker 先抢共享的驱动,抢到的睡在驱动上,抢不到的睡在自己的条件变量上,见任务窃取与空闲。 - 忙,但每
event_interval(默认 61)个 tick 看一眼。 current_thread 的park_yield和多线程Parker的park_driver都传超时 0:driver.park_timeout(handle, Duration::ZERO)。
超时为 0 的这一趟在两种最底层上代价差别很大:
- 开了 IO:时间驱动处理到期定时器,IO 驱动做一次不等待的
Poll::poll,即一次epoll_wait/kevent系统调用,超时为 0。如果设了max_io_events_per_busy_tick,这一趟用它单独的事件缓冲区。 - 没开 IO:
ParkThread的park_timeout先做一次 CAS 消费可能存在的通知,然后if dur == Duration::from_millis(0) { return; },不加锁,不进内核。
平台是 M2 Max、rustc 1.99.0。每段用一个新建的 current_thread 运行时,只开时间驱动(所以最底层是 ParkThread),用 on_thread_park 数进入 park 的次数:
// ①
r.block_on(async {
for _ in 0..1000 {
tokio::task::yield_now().await;
}
});
// ②
r.block_on(async {
for _ in 0..5 {
tokio::time::sleep(Duration::from_millis(10)).await;
}
});
// ③
r.block_on(async {
let mut handles = Vec::new();
for _ in 0..5 {
handles.push(tokio::spawn(tokio::time::sleep(Duration::from_millis(10))));
}
for h in handles {
h.await.unwrap();
}
});
提示:on_thread_park 只在 Context::park 里调用,超时为 0 的 park_yield 不调。再想想:往时间轮里插一个比现有定时器都早的定时器时,会不会顺手叫醒谁?答案在本页末尾。
叫醒:从 Handle 一路到 Poll
pub(crate) fn unpark(&self) {
#[cfg(feature = "time")]
if let Some(handle) = &self.time {
handle.unpark();
}
self.io.unpark();
}
时间句柄的 unpark 只在 test-util 下记一个 did_wake 标志,供暂停时钟时判断「是被叫醒的还是超时到了」。真正叫醒线程的是 IoHandle::unpark,按最底层分两路:
pub(crate) fn unpark(&self) {
match self {
IoHandle::Enabled(handle) => handle.unpark(),
IoHandle::Disabled(handle) => handle.unpark(),
}
}
开了 IO:io::Handle::unpark 是一行 self.waker.wake()。这个 mio::Waker 在 io::Driver::new 里用 TOKEN_WAKEUP(Token(0))注册到同一个 Poll 上。mio 1.x 在 Linux 上用 eventfd 实现它,在 macOS 和 BSD 上用 kqueue 的 EVFILT_USER 事件。wake() 让这个 fd 或事件就绪,阻塞在 Poll::poll 里的线程于是返回。turn 看到 TOKEN_WAKEUP 什么也不做,它的作用只是让 poll 返回:
if token == TOKEN_WAKEUP {
// Nothing to do, the event is used to unblock the I/O driver
} else if token == TOKEN_SIGNAL {
self.signal_ready = true;
} else {
如果调 wake() 时没人在 poll 里,事件留在内核里,下一次 poll 立即返回,通知不会丢。
没开 IO:UnparkThread::unpark 调 ParkThread 内部的 Inner::unpark,下一节讲。
调用方有两类:current_thread 在非本运行时线程上调度任务时,schedule 把任务推进注入队列后调 self.driver.unpark();多线程 Parker 的 unpark 发现目标 worker 状态是 PARKED_DRIVER 时调 driver.unpark()。在本线程上调度时,current_thread 调的是 unpark_self,它只碰时间句柄,不调 io.unpark():线程自己醒着,没必要做一次系统调用叫醒自己。
unpark_self 是修一个 bug 时加的1.50.0 的 #7834 让 current_thread 在运行时自己的上下文里唤醒 block_on 的 future 时,不再调 driver.unpark()。起因是 issue #7570:一个 current_thread 的 TCP 服务在 strace 下,每来一个请求都多出一次 write 系统调用。
但 driver::Handle::unpark 做两件事,跳过它也就跳过了时间句柄上的 did_wake。issue #8232 报告,升到 1.50 以后,#[tokio::test(start_paused = true)] 下的 timeout 提前超时。#8595 的说明把链条讲清了:时钟暂停时,时间驱动先做一次超时为 0 的 park,期间如果有 IO 事件唤醒了任务,did_wake 仍是 false,驱动就以为没人被叫醒,把虚拟时钟往前拨了。修法就是 unpark_self:只记 did_wake,不碰 IO,不做系统调用。它最早随 1.51.6 发布,之后的 1.53.3 和 1.54.0 也带着。
ParkThread:一个原子变量加一个条件变量
park.rs 里的 ParkThread 和 UnparkThread 共享一个 Arc<Inner>:
struct Inner {
state: AtomicUsize,
mutex: Mutex<()>,
condvar: Condvar,
// ...
}
const EMPTY: usize = 0;
const PARKED: usize = 1;
const NOTIFIED: usize = 2;
这是经典的「原子状态加条件变量」做法。三个状态:
stateDiagram-v2 EMPTY --> PARKED: park,加锁后 CAS PARKED --> NOTIFIED: unpark,swap EMPTY --> NOTIFIED: unpark 先到 NOTIFIED --> EMPTY: park 消费通知
ParkThread 像一间值班室的门。想睡的人(park)先看门上有没有便条(NOTIFIED),有就撕掉,不睡了。没有,就挂上「睡觉中」的牌子(PARKED)再躺下。
来叫人的人(unpark)一律先在门上贴一张便条(swap(NOTIFIED)),看到原来挂的是「睡觉中」才敲门(notify_one),否则贴完就走。敲门前先拧一下门锁(drop(self.mutex.lock())):屋里的人是锁着门挂牌、躺下时才松开门锁的,拧得开就说明他已经躺下,敲门不会落空。
fn park(&self) {
// If we were previously notified then we consume this notification and
// return quickly.
if self
.state
.compare_exchange(NOTIFIED, EMPTY, SeqCst, SeqCst)
.is_ok()
{
return;
}
// Otherwise we need to coordinate going to sleep
let mut m = self.mutex.lock();
match self.state.compare_exchange(EMPTY, PARKED, SeqCst, SeqCst) {
Ok(_) => {}
Err(NOTIFIED) => {
// ...
let old = self.state.swap(EMPTY, SeqCst);
debug_assert_eq!(old, NOTIFIED, "park state changed unexpectedly");
return;
}
Err(actual) => panic!("inconsistent park state; actual = {actual}"),
}
loop {
m = self.condvar.wait(m).unwrap();
if self
.state
.compare_exchange(NOTIFIED, EMPTY, SeqCst, SeqCst)
.is_ok()
{
// got a notification
return;
}
// spurious wakeup, go back to sleep
}
}
fn unpark(&self) {
// ...
match self.state.swap(NOTIFIED, SeqCst) {
EMPTY => return, // no one was waiting
NOTIFIED => return, // already unparked
PARKED => {} // gotta go wake someone up
_ => panic!("inconsistent state in unpark"),
}
// ...
drop(self.mutex.lock());
self.condvar.notify_one();
}
几个设计点:
- 通知先到也不丢。 unpark 在对方 park 之前发生,状态停在
NOTIFIED;park 的第一步 CAS 就会消费它,直接返回。这就是CachedParkThread::block_on可以放心地「poll 一次、没好就 park」的原因:poll 期间 waker 被调用了,park 立刻返回,再 poll 一次。 - unpark 只在对方真睡了时才碰锁。 旧状态是
EMPTY或NOTIFIED时,一次swap就结束,不加锁不进内核。 - 那句
drop(self.mutex.lock())。 park 一方在持锁状态下把状态改成PARKED,再在condvar.wait里释放锁。如果 unpark 在「改成PARKED」和「开始 wait」之间调notify_one,通知会落空,对方随后睡下就再也醒不过来。unpark 先拿一次锁,就保证对方已经进入 wait(锁已释放);拿到锁后立刻放掉再 notify,对方醒来时不用再等这把锁。 swap而不是 CAS。 注释说明:即使已经是NOTIFIED,也要写一次,这样 park 一方读到这个值时能和 unpark 之前的所有写入建立 happens-before 关系。- 虚假唤醒。
condvar.wait可能无故返回,所以放在循环里,只有 CAS 到NOTIFIED → EMPTY才算真醒。park_timeout不循环:超时到了也算结束,最后统一swap(EMPTY)。
shutdown 是 condvar.notify_all(),运行时关闭时叫醒所有睡在这上面的线程。
两个 ParkThread
ParkThread 在运行时里有两种用法,别混了:
| 用法 | 实例在哪 | 谁睡在上面 |
|---|---|---|
| 驱动栈最底层(没开 IO) | IoStack::Disabled 里,每个运行时一个 | 拿着驱动的那个调度线程 |
CachedParkThread | 线程本地 CURRENT_PARKER,每个线程一个 | Handle::block_on、多线程的 Runtime::block_on、blocking_recv 这类的调用线程 |
CachedParkThread 的 waker 直接由 Arc<Inner> 做成:unparker_to_raw_waker 手写了一张 RawWakerVTable,clone 是 Arc 计数加一,wake 是 Inner::unpark。所以 block_on 的 future 被唤醒,等于往那个线程的条件变量上发一次通知,不经过任何队列。
问: 标准库已经有 std::thread::park 和 Thread::unpark,语义也是「先 unpark 后 park 不会丢」。tokio 为什么还要自己写一个 ParkThread?
答: 标准库的 unpark 要先拿到目标线程的 Thread,也就是要知道是谁睡在上面。驱动栈最底层的那个 ParkThread 属于运行时,不属于哪个线程:create_io_stack 在 build() 时就把它的 UnparkThread 放进了 IoHandle::Disabled,那时还不知道以后哪个线程会睡上去。多线程运行时里,哪个 worker 抢到驱动(TryLock),哪个 worker 就睡在这同一个 ParkThread 上,下一次可能换一个。用标准库的话,每次 park 前都得把「现在睡的是谁」登记到一个共享的地方,unpark 时再去读,而且登记和读之间还有竞争要处理。Inner 把状态和条件变量放在一个 Arc 里,谁拿着它谁就能睡,谁拿着 UnparkThread 谁就能叫,不用知道对方是哪个线程。
实验:没有 IO 驱动时的 park 和 unpark
一个普通线程给 current_thread 运行时里的 block_on future 发消息,future 收到后用标准库的 channel 回一条,来回 10 万次。用 on_thread_park 回调数运行时线程进入 park 的次数。分别在关 IO 和开 IO 下各跑两遍,最后顺便看看没开驱动时的两句 panic。src/bin/park_unpark.rs:
use std::panic;
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use std::sync::Arc;
use std::sync::mpsc as std_mpsc;
use std::thread;
use std::time::{Duration, Instant};
use tokio::runtime::Builder;
use tokio::sync::mpsc;
const ROUNDS: u32 = 100_000;
// 普通线程发消息给 current_thread 运行时里的任务,任务再用 std channel 回一条。
// 每一轮运行时线程都会睡下去,再被普通线程叫醒。
fn ping_pong(io: bool) -> (Duration, usize) {
let parks = Arc::new(AtomicUsize::new(0));
let p2 = parks.clone();
let mut b = Builder::new_current_thread();
b.on_thread_park(move || {
p2.fetch_add(1, Relaxed);
});
if io {
b.enable_io();
}
let rt = b.build().unwrap();
let (tx, mut rx) = mpsc::unbounded_channel::<u32>();
let (back_tx, back_rx) = std_mpsc::channel::<u32>();
let t = thread::spawn(move || {
let start = Instant::now();
for i in 0..ROUNDS {
tx.send(i).unwrap();
back_rx.recv().unwrap();
}
start.elapsed()
});
rt.block_on(async move {
while let Some(i) = rx.recv().await {
back_tx.send(i).unwrap();
}
});
(t.join().unwrap(), parks.load(Relaxed))
}
fn main() {
for io in [false, true, false, true] {
let (d, parks) = ping_pong(io);
println!(
"enable_io = {io:<5} {ROUNDS} rounds parks {parks} {:>4} ms {:.2} us/round",
d.as_millis(),
d.as_secs_f64() * 1e6 / ROUNDS as f64
);
}
panic::set_hook(Box::new(|info| {
let msg = info.payload().downcast_ref::<&str>().map(|s| s.to_string())
.or_else(|| info.payload().downcast_ref::<String>().cloned()).unwrap_or_default();
println!("panic: {msg}");
}));
let rt = Builder::new_current_thread().build().unwrap();
let _ = panic::catch_unwind(panic::AssertUnwindSafe(|| {
rt.block_on(async { tokio::time::sleep(Duration::from_millis(1)).await });
}));
let _ = panic::catch_unwind(panic::AssertUnwindSafe(|| {
rt.block_on(async { tokio::net::TcpListener::bind("127.0.0.1:0").await.map(|_| ()) }).unwrap();
}));
}
输出(Apple M2 Max,macOS,release 构建):
enable_io = false 100000 rounds parks 99904 430 ms 4.30 us/round
enable_io = true 100000 rounds parks 99886 324 ms 3.24 us/round
enable_io = false 100000 rounds parks 99876 432 ms 4.33 us/round
enable_io = true 100000 rounds parks 99900 319 ms 3.19 us/round
panic: A Tokio 1.x context was found, but timers are disabled. Call `enable_time` on the runtime builder to enable timers.
panic: A Tokio 1.x context was found, but IO is disabled. Call `enable_io` on the runtime builder to enable IO.
- 关掉 IO 和时间,运行时照样能睡、能被别的线程叫醒。10 万轮里约 99,900 轮进了 park(
on_thread_park在Context::park开头调用),其余几十轮是 future 再被 poll 时消息已经到了。 - 两种栈的最底层不同:关 IO 时睡在
ParkThread的条件变量上,tx.send唤醒block_on的 waker,走driver.unpark()→UnparkThread→notify_one;开 IO 时睡在kevent里,唤醒走 mioWaker,在 macOS 上是一次EVFILT_USER触发。 - 在这台机器上两条路都是每轮 3 到 5 微秒。多跑几次,关 IO 在 4.1 到 4.7 微秒之间,开 IO 在 3.1 到 4.0 之间,开 IO 略快。这个数包括两次跨线程唤醒(一来一回),差别取决于平台的条件变量和 kqueue 实现,不能推到 Linux 上。结论只有一条:没开 IO 不会让唤醒变慢到需要担心的程度,省下的是 mio 的
Poll和每 61 个 tick 一次的kevent系统调用。
动脑筋答案
完整程序(src/bin/park_count.rs):
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use std::sync::Arc;
use std::time::Duration;
use tokio::runtime::{Builder, Runtime};
fn rt(parks: Arc<AtomicUsize>) -> Runtime {
Builder::new_current_thread()
.enable_time()
.on_thread_park(move || {
parks.fetch_add(1, Relaxed);
})
.build()
.unwrap()
}
fn main() {
let parks = Arc::new(AtomicUsize::new(0));
let r = rt(parks.clone());
r.block_on(async {
for _ in 0..1000 {
tokio::task::yield_now().await;
}
});
println!("1000 x yield_now -> parks {}", parks.swap(0, Relaxed));
let r = rt(parks.clone());
r.block_on(async {
for _ in 0..5 {
tokio::time::sleep(Duration::from_millis(10)).await;
}
});
println!("5 x sleep(10ms) -> parks {}", parks.swap(0, Relaxed));
let r = rt(parks.clone());
r.block_on(async {
let mut handles = Vec::new();
for _ in 0..5 {
handles.push(tokio::spawn(tokio::time::sleep(Duration::from_millis(10))));
}
for h in handles {
h.await.unwrap();
}
});
println!("5 spawned sleep(10ms) -> parks {}", parks.swap(0, Relaxed));
}
--release 连跑 3 次,输出都一样:
1000 x yield_now -> parks 0
5 x sleep(10ms) -> parks 10
5 spawned sleep(10ms) -> parks 2
① 0 次。
yield_now在调度器上下文里不会马上唤醒自己,而是把 waker 放进 defer 列表。队列空了、defer 不空,block_on走的是park_yield:超时为 0 地看一眼驱动,顺手唤醒 defer 里的 waker。1000 次yield_now一次也没进Context::park,on_thread_park就一次也没调。② 10 次,每个
sleep两次。sleep第一次被 poll 时把定时器插进时间轮。reregister发现它比next_wake早(上一个定时器触发后next_wake是空的),就调一次unpark.unpark(),ParkThread的状态变成NOTIFIED。接下来第一次 park 在第一步 CAS 就消费了这个通知,立刻返回;时间驱动醒来一看定时器没到期,调度器只好再 park 一次,这次才真正睡到定时器到期。把on_thread_park和on_thread_unpark之间的时间记下来(src/bin/park_dur.rs,3 个sleep(10ms)):.on_thread_park(move || { *t1.lock().unwrap() = Some(Instant::now()); }) .on_thread_unpark(move || { let s = t2.lock().unwrap().take().unwrap(); l2.lock().unwrap().push(s.elapsed()); })一短一长交替出现:短的那次不到 1 微秒,就是那个提前到达的通知;长的那次才是真睡。连跑 3 次都是这个模式,长的那次每次在 11 到 14 毫秒之间,比 10 毫秒长,这里不展开。[625ns, 12.216833ms, 333ns, 12.924458ms, 333ns, 13.764709ms]③ 2 次。 5 个任务各插一个定时器,每次插入都调了
unpark,但ParkThread只有一个状态值,5 次通知叠成一个NOTIFIED。于是还是一次空 park 加一次真睡,5 个定时器在同一次唤醒里一起触发。
小结
- 驱动是一个按值嵌套的栈:时间 → 进程 → 信号 → IO → mio
Poll。层与层之间用枚举分派,不用 trait 对象。 - feature flag 在编译期删层,
enable_io/enable_time在运行时删层。没开 IO 时最底层是ParkThread,信号和进程两层也一起没有。 Driver独占,归调度器;driver::Handle共享,用来注册资源和叫醒。没开某个驱动时取它的句柄会 panic「A Tokio 1.x context was found, but ... is disabled」。- park 时每层先调下一层、返回后做自己的事。时间驱动把「最近定时器到期还有多久」作为超时传下去,自己从不睡。
- 叫醒:开 IO 时是 mio
Waker(Linux 上是 eventfd,macOS 上是 kqueueEVFILT_USER),没开时是ParkThread的条件变量。两种都保证「先通知后睡」不会丢。 ParkThread是 EMPTY / PARKED / NOTIFIED 三态加Mutex和Condvar。unpark 先拿一次锁再 notify,堵住「改了状态还没开始 wait」那个窗口。