Rust in Everything
基于 tokio 1.54.1

驱动栈与 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 分派,都是静态调用。

📝NOTE
冷知识:这里原来有一个 Park trait

1.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 flagIO 相关的 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_timeIoStack::Disabled、TimeDriver::Disabled

在开了全部 feature 的 Unix 上,运行时组合出来是四种:

enable_timeenable_io栈最底层
开开时间 → 进程 → 信号 → IOmio Poll
开关时间 → ParkThread条件变量
关开进程 → 信号 → IOmio 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);
}

signal::Driver::park:

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(最近到期) 或 parkprocess:触发到期定时器的 waker
进程—park / park_timeoutreap_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; },不加锁,不进内核。
❗IMPORTANT
动脑筋:三段程序各进几次 park?

平台是 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

driver::Handle::unpark:

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():线程自己醒着,没必要做一次系统调用叫醒自己。

📝NOTE
冷知识: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 消费通知
💡TIP
打个比方:值班室门上的便条

ParkThread 像一间值班室的门。想睡的人(park)先看门上有没有便条(NOTIFIED),有就撕掉,不睡了。没有,就挂上「睡觉中」的牌子(PARKED)再躺下。

来叫人的人(unpark)一律先在门上贴一张便条(swap(NOTIFIED)),看到原来挂的是「睡觉中」才敲门(notify_one),否则贴完就走。敲门前先拧一下门锁(drop(self.mutex.lock())):屋里的人是锁着门挂牌、躺下时才松开门锁的,拧得开就说明他已经躺下,敲门不会落空。

比方到这里为止:真便条贴十张就是十张,这里只有一个状态值。连着 unpark 十次,下一次 park 只会少睡一次。上面「动脑筋」第 ③ 段的答案就和这一点有关。

Inner::park:

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
    }
}

Inner::unpark:

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 被唤醒,等于往那个线程的条件变量上发一次通知,不经过任何队列。

📝NOTE
没有蠢问题

问: 标准库已经有 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 里,唤醒走 mio Waker,在 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()); })
    
    [625ns, 12.216833ms, 333ns, 12.924458ms, 333ns, 13.764709ms]
    
    一短一长交替出现:短的那次不到 1 微秒,就是那个提前到达的通知;长的那次才是真睡。连跑 3 次都是这个模式,长的那次每次在 11 到 14 毫秒之间,比 10 毫秒长,这里不展开。
  • ③ 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 上是 kqueue EVFILT_USER),没开时是 ParkThread 的条件变量。两种都保证「先通知后睡」不会丢。
  • ParkThread 是 EMPTY / PARKED / NOTIFIED 三态加 Mutex 和 Condvar。unpark 先拿一次锁再 notify,堵住「改了状态还没开始 wait」那个窗口。

💬 关联讨论

发起讨论
加载中...