Rust in Everything
基于 tokio 1.54.1

Builder、Runtime 与 Handle

主线位置:任务主线的起点。#[tokio::main] 展开后的第一步就是 Builder::build,之后所有 spawn 都要先拿到一个 Handle。

flowchart TB
  rt[Runtime] --> sched["Scheduler 枚举"]
  rt --> h[Handle]
  rt --> pool[BlockingPool]
  sched --> ct["CurrentThread:持有 Core"]
  sched --> mt["MultiThread:空结构体"]
  h --> sh["scheduler::Handle 枚举"]
  sh --> cth["Arc ct::Handle"]
  sh --> mth["Arc mt::Handle"]
  cth --> parts[驱动句柄、阻塞池入口、队列]
  mth --> parts

#[tokio::main] 展开成什么、build() 的主干、worker 为什么是阻塞线程池里的线程,在从 #[tokio::main] 开始里讲过。这一章补上三件事:配置项的全貌、Runtime 和 Handle 各自持有什么、Handle 为什么能跨线程用。

💡TIP
打个比方:一家只有一口灶的小饭馆

Runtime 是老板,手里有三样东西:后厨(Scheduler)、杂工房(BlockingPool)和一叠点菜单(Handle)。点菜单可以随便复印(clone,Arc 计数加一),谁拿着都能把单子递进出菜口(spawn)。灶只有一口(Driver),等 IO、等定时器都要靠它开火,同一时间只有一个人站在灶前。

current_thread 的灶锁在老板的后厨里(Core),只有老板亲自下厨(Runtime::block_on)时才点火。拿着点菜单在门口等(Handle::block_on)的人,等的东西要是需要灶火(IO、定时器),就一直等不到。multi_thread 的灶放在几个厨师(worker)都进得去的地方(Parker),谁闲下来谁去看火。

比方到这里为止:真饭馆拆了,点菜单就是废纸。tokio 的 Handle 还攥着 Arc,运行时的内存要等最后一张单子扔掉才释放;只是这时再递单子,任务会被立刻取消。

配置项:按管什么分组

Builder 是一个普通结构体,每个配置方法只是改一个字段。默认值全部写在 Builder::new 里。两个入口 new_current_thread 和 new_multi_thread 只差 kind 一个字段,event_interval 都传 61:

pub fn new_multi_thread() -> Builder {
    // The number `61` is fairly arbitrary. I believe this value was copied from golang.
    Builder::new(Kind::MultiThread, 61)
}

下面四张表里,「unstable」表示需要 --cfg tokio_unstable 才有这个方法。

线程

方法默认说明
worker_threadsNone为 None 时调 num_cpus:先读环境变量 TOKIO_WORKER_THREADS,没有再用 available_parallelism(),它也失败就是 1。传 0 会 panic。current_thread 忽略这一项
max_blocking_threads512阻塞线程上限。多线程运行时的线程池实际上限是 512 + worker_threads,current_thread 就是 512
thread_keep_aliveNone为 None 时用 blocking/pool.rs 里的 KEEP_ALIVE,10 秒。空闲阻塞线程等这么久没活就退出
thread_name / thread_name_fn"tokio-rt-worker"worker 和阻塞线程用同一个名字,因为 worker 本身就是阻塞池里的线程
thread_stack_sizeNone不设就用标准库默认值
nameNone运行时的名字,Handle::name() 读得到
📝NOTE
冷知识:512 + worker_threads 这个加法是 1.0 才有的

0.2 里这一项叫 max_threads,数的是线程池里的全部线程,worker 也算在内。2020 年 8 月的 issue #2802 报告:把 core_threads(当时 worker 数的名字)和 max_threads 设成一样大,名额全被 worker 占了,spawn_blocking 返回的 future 永远不会完成。issue 讨论里提出把它改名为 max_blocking_threads,总线程数自动变成 worker 数加上它。#3287(1.0.0)照此改名,默认值 512 没变,build_threaded_runtime 从此把 max_blocking_threads + core_threads 传给 create_blocking_pool。

调度

方法默认说明
event_interval61每多少个 tick 去驱动上看一次 IO 和定时器,两种调度器相同
global_queue_intervalNonecurrent_thread 为 31;多线程按任务平均 poll 耗时自动调,见全局注入队列
disable_lifo_slot(unstable)false关掉 LIFO 槽,见 worker 主循环
unhandled_panic(unstable)IgnoreShutdownRuntime 只支持 current_thread
rng_seed(unstable)随机固定调度器里随机数的种子,让 select! 之类的选择顺序可复现
enable_eager_driver_handoff(unstable)false抢到驱动的 worker 要开始跑任务时,先叫醒另一个 worker 接手驱动。只对多线程有效
enable_sharded_blocking_queue(unstable)看环境变量阻塞池队列分片;环境变量 TOKIO_UNSTABLE_SHARDED_BLOCKING_QUEUE 非空且不为 0 时默认打开

驱动

方法默认说明
enable_iofalse不开就没有 mio,TcpStream 之类会 panic
enable_timefalse不开就没有时间轮,sleep 会 panic
enable_all—打开 IO 和时间;在 Linux 加 tokio_unstable 和 io-uring feature 时还会打开 io_uring
max_io_events_per_tick1024一次 Poll::poll 最多取多少个事件
max_io_events_per_busy_tickNone不等待的那种 poll(超时为 0)单独用的事件数
start_paused(test-util feature)false时钟一开始就暂停。只有 current_thread 允许暂停时钟
enable_alt_timer(unstable)关换用另一套定时器实现,只对多线程有效,见 (unstable)time_alt

最后一行说的「只有 current_thread 允许暂停」来自 get_cfg,它把 Builder 上和驱动有关的字段抄进 driver::Cfg:

fn get_cfg(&self) -> driver::Cfg {
    driver::Cfg {
        enable_pause_time: match self.kind {
            Kind::CurrentThread => true,
            #[cfg(feature = "rt-multi-thread")]
            Kind::MultiThread => false,
        },
        enable_io: self.enable_io,
        enable_time: self.enable_time,
        start_paused: self.start_paused,
        nevents: self.nevents,
        nevents_busy: self.nevents_busy,
        timer_flavor: self.timer_flavor,
    }
}
📝NOTE
冷知识:max_io_events_per_busy_tick 是 1.54.0 才加的

加它的是 2026 年 9 月的 #8410。之前只有 max_io_events_per_tick 一项:worker 手上还有任务时,每 event_interval(61)个任务看一次驱动,这一趟也最多取 1024 个事件。PR 说明算了这笔账:这些事件唤醒的任务进 worker 的本地队列,本地队列只有 256 格,多出来的溢出到全局队列;持续过载时积压越来越多,直到请求超时。把 max_io_events_per_tick 调到 8 能止住溢出,但真正睡下去的那次 park 也只取 8 个事件,很多连接同时就绪时延迟变大。

新选项只管超时为 0 的那种 poll,阻塞的 park 仍按 max_io_events_per_tick 取。IO 驱动为此多备一个小一些的 mio::Events 缓冲区。不设这一项时,两种 poll 都用 max_io_events_per_tick,和以前一样。

回调:on_thread_start / on_thread_stop(线程启动、退出)、on_thread_park / on_thread_unpark(调度线程睡前、醒后)、on_task_spawn / on_task_terminate(任务创建、结束),以及 unstable 的 on_before_task_poll / on_after_task_poll。剩下的方法都是 unstable 的指标直方图配置,不影响调度行为。

IO 和时间默认都关着,这是有意的:只用 tokio 跑 CPU 任务、或者只用 spawn_blocking 的程序,不必为 mio 的 Poll 和时间轮付出代价。关掉以后最底层换成什么,见驱动栈与 park。

build():两种调度器,同一个顺序

build 按 kind 分两支。多线程那支 build_threaded_runtime 已经在 01 里贴过。current_thread 这支是 build_current_thread_runtime_components:

let mut cfg = self.get_cfg();
cfg.timer_flavor = TimerFlavor::Traditional;
let (driver, driver_handle) = driver::Driver::new(cfg)?;

// Blocking pool
let blocking_pool = blocking::create_blocking_pool(self, self.max_blocking_threads, 0);
let blocking_spawner = blocking_pool.spawner().clone();

// Generate a rng seed for this runtime.
let seed_generator_1 = self.seed_generator.next_generator();
let seed_generator_2 = self.seed_generator.next_generator();

// And now put a single-threaded scheduler on top of the timer. When
// there are no futures ready to do something, it'll let the timer or
// the reactor to generate some new stimuli for the futures to continue
// in their life.
let (scheduler, handle) = CurrentThread::new(
    driver,
    driver_handle,
    blocking_spawner,
    seed_generator_2,
    Config { /* ... */ },
    local_tid,
    self.name.clone(),
);

let handle = Handle {
    inner: scheduler::Handle::CurrentThread(handle),
};

两支的顺序一样:

步骤current_threadmulti_thread
1. 驱动Driver::new,定时器强制用传统实现Driver::new
2. 阻塞线程池上限 max_blocking_threads,不给 worker 留位置上限 max_blocking_threads + worker_threads
3. 调度器CurrentThread::new,驱动放进 CoreMultiThread::new,驱动放进所有 worker 共享的 Parker
4. 启动不启动任何线程launch(),每个 worker 作为一个阻塞任务启动

Driver::new 返回一对东西:Driver 本身(只有一个,谁拿着谁才能 park 在上面)和 driver::Handle(可以共享,用来注册 IO、注册定时器、叫醒睡着的线程)。前者交给调度器的 Core 或 Parker,后者放进调度器的 Handle。这个拆分贯穿整个运行时:独占的部分归调度器,共享的部分归句柄。

current_thread 的 build() 不创建任何线程。在 block_on 之前,spawn 出去的任务只是进了队列,没有人跑。

❗IMPORTANT
动脑筋:三个任务跑了几个?
static RAN: AtomicUsize = AtomicUsize::new(0);

fn spawn_three(rt: &tokio::runtime::Runtime) {
  for _ in 0..3 {
    rt.spawn(async {
      RAN.fetch_add(1, Relaxed);
    });
  }
}

// A
let rt = Builder::new_current_thread().build().unwrap();
spawn_three(&rt);
rt.block_on(async {});

// B
let rt = Builder::new_current_thread().build().unwrap();
spawn_three(&rt);
rt.block_on(tokio::task::yield_now());

// C
let rt = Builder::new_multi_thread().worker_threads(2).build().unwrap();
spawn_three(&rt);
std::thread::sleep(std::time::Duration::from_millis(50));

A、B、C 三段结束时,各有几个任务跑过(RAN 是多少)?每段之前 RAN 清零。

提示:current_thread 的 block_on 进了循环,先 poll 的是谁?答案在本页末尾。

Runtime:三样东西的拥有者

Runtime 只有三个字段:

pub struct Runtime {
    /// Task scheduler
    scheduler: Scheduler,

    /// Handle to runtime, also contains driver handles
    handle: Handle,

    /// Blocking pool handle, used to signal shutdown
    blocking_pool: BlockingPool,
}

Scheduler 是两个变体的枚举。两个变体的分量差别很大:

  • CurrentThread 持有 core: AtomicCell<Core>,Core 里有本地队列和驱动。谁调 Runtime::block_on 谁就把 Core 拿走去跑(见 current_thread)。
  • MultiThread 是一个空结构体:pub(crate) struct MultiThread;。它的所有状态都在 Handle 里,每个 worker 的 Core 由 worker 线程自己拿着。

Runtime 上的方法大多直接转给 handle:spawn、spawn_blocking、enter、metrics 都是。只有两个方法真正用到 scheduler:block_on 和 Drop。

block_on_inner 先 self.enter() 设好当前句柄,再按调度器类型分派:

let _enter = self.enter();

match &self.scheduler {
    Scheduler::CurrentThread(exec) => exec.block_on(&self.handle.inner, future),
    #[cfg(feature = "rt-multi-thread")]
    Scheduler::MultiThread(exec) => exec.block_on(&self.handle.inner, future),
}

current_thread 的 exec.block_on 会去抢 Core,抢到了就在当前线程上跑调度循环、驱动 IO 和定时器。Handle 上也有 block_on,但它摸不到 Scheduler,这个差别下面的实验会看到。

📝NOTE
没有蠢问题

问: 多线程运行时上,#[tokio::main] 的 main 函数体跑在哪个 worker 上?

答: 哪个都不在。MultiThread::block_on 只是 enter_runtime 加 CachedParkThread::block_on,它上面的注释写得很直接:「The future will execute on the current thread, but all spawned tasks will be executed on the thread pool.」main 函数体这个 future 一直在主线程上被 poll,worker_threads(4) 的 4 个 worker 只跑 spawn 出去的任务。所以在 main 里做一段 CPU 计算不会占掉 worker,也没有别的 worker 能分担它;要和别的任务并行,得 spawn 出去。

Drop 也按调度器分派:

impl Drop for Runtime {
    fn drop(&mut self) {
        match &mut self.scheduler {
            Scheduler::CurrentThread(current_thread) => {
                // This ensures that tasks spawned on the current-thread
                // runtime are dropped inside the runtime's context.
                let _guard = context::try_set_current(&self.handle.inner);
                current_thread.shutdown(&self.handle.inner);
            }
            #[cfg(feature = "rt-multi-thread")]
            Scheduler::MultiThread(multi_thread) => {
                // The threaded scheduler drops its tasks on its worker threads, which is
                // already in the runtime's context.
                multi_thread.shutdown(&self.handle.inner);
            }
        }
    }
}

current_thread 在 drop 的线程上就地关掉所有任务,所以先把当前句柄设好:任务的 Drop 里如果调了 tokio::spawn,也能找到运行时。多线程只是发个关闭信号,任务在各自的 worker 线程上被 drop。之后字段按声明顺序 drop,BlockingPool 的 Drop 调 shutdown(None),没有超时地等所有阻塞线程退出,worker 线程也在其中。这就是为什么一个卡住的 spawn_blocking 会让 drop(runtime) 卡住;不想等就用 shutdown_timeout 或 shutdown_background(等价于超时为 0)。关闭时任务怎么被逐个取消,见 OwnedTasks 与关闭。

Handle:一个包着 Arc 的枚举

公开的 Handle 只包了一层:

#[derive(Debug, Clone)]
pub struct Handle {
    pub(crate) inner: scheduler::Handle,
}

里面的 scheduler::Handle 才是正主:

#[derive(Debug, Clone)]
pub(crate) enum Handle {
    #[cfg(feature = "rt")]
    CurrentThread(Arc<current_thread::Handle>),

    #[cfg(feature = "rt-multi-thread")]
    MultiThread(Arc<multi_thread::Handle>),

    // TODO: This is to avoid triggering "dead code" warnings many other places
    // in the codebase. Remove this during a later cleanup
    #[cfg(not(feature = "rt"))]
    #[allow(dead_code)]
    Disabled,
}

两种调度器没有共同的 trait,而是用枚举加 match 分派。spawn 是典型:

match self {
    Handle::CurrentThread(h) => current_thread::Handle::spawn(h, future, id, spawned_at),

    #[cfg(feature = "rt-multi-thread")]
    Handle::MultiThread(h) => multi_thread::Handle::spawn(h, future, id, spawned_at),
}

更短的那些用一个 match_flavor! 宏生成同样的两个分支。变体固定只有两个,枚举比 Arc<dyn Trait> 少一次虚调用,spawn 这种泛型方法也没法放进 trait 对象。

两个 Arc 指向的结构体字段几乎一样。多线程的 multi_thread::Handle:

字段内容
shared注入队列、每个 worker 的远端句柄(偷任务用)、OwnedTasks、空闲状态
driverdriver::Handle:IO、信号、时间三个句柄和时钟
blocking_spawner往阻塞线程池提交任务的入口
seed_generator每次进入运行时分配一个随机数种子
task_hookson_task_spawn 等回调
name运行时名字

current_thread 的 current_thread::Handle 多一个 local_tid,给 LocalRuntime 检查调用线程用。

这些字段全是可以共享的东西:队列带锁,计数器是原子变量,驱动句柄本身就是给多个线程用的。所以 Handle 自然是 Send + Sync,clone 只是把 Arc 的引用计数加一。整个 Handle 就是一个判别值加一个指针:

size_of::<Handle>() = 16

(下面实验的第一行输出,64 位 macOS。)

Handle 持有 Arc,所以运行时的内存在最后一个 Handle 消失之前不会释放。但它不能让运行时「继续运行」:Runtime 被 drop 时照样关闭,之后通过旧 Handle spawn 的任务会被立即取消。

在别的线程上用 Handle

拿到一个 Handle,在任何线程上都能做这几件事:

方法做什么
spawn走 scheduler::Handle::spawn。从非 worker 线程调用,任务进注入队列,再叫醒一个 worker
spawn_blocking交给 blocking_spawner
enter把它设成当前线程的「当前运行时」,返回 EnterGuard,之后 tokio::spawn、Handle::current() 都能用,见线程本地上下文
block_on在调用线程上跑一个 future

Handle::block_on 和 Runtime::block_on 不一样,它不碰调度器。函数体在 block_on_inner 里:

// Enter the runtime context. This sets the current driver handles and
// prevents blocking an existing runtime.
context::enter_runtime(&self.inner, true, |blocking| {
    blocking.block_on(future).expect("failed to park thread")
})

blocking.block_on 是 CachedParkThread::block_on:poll 一次,没好就让线程睡在一个条件变量上。对多线程运行时,这和 Runtime::block_on 没区别,因为多线程的 Runtime::block_on 也是这样做的,IO 和定时器由 worker 去驱动。对 current_thread,驱动在 Core 里,Core 在 Runtime 手上,Handle::block_on 拿不到,所以没有人去驱动 IO 和定时器,除非另一个线程正在调 Runtime::block_on。

实验(src/bin/handle_threads.rs):

use std::thread;
use std::time::{Duration, Instant};
use tokio::runtime::{Builder, Handle};

fn name() -> String {
  thread::current().name().unwrap_or("?").to_string()
}

fn main() {
  println!("size_of::<Handle>() = {}", std::mem::size_of::<Handle>());

  // multi_thread:Handle 拿到别的线程上用
  let rt = Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap();
  let handle = rt.handle().clone();
  let t = thread::Builder::new().name("plain-thread".into()).spawn(move || {
    let jh = handle.spawn(async { name() });
    let on = handle.block_on(async move {
      let spawned_on = jh.await.unwrap();
      (name(), spawned_on)
    });
    println!("[mt] block_on future ran on {}, spawned task ran on {}", on.0, on.1);
  }).unwrap();
  t.join().unwrap();

  // Runtime drop 之后,Handle 还在
  let handle = rt.handle().clone();
  drop(rt);
  let jh = handle.spawn(async { 42 });
  let r = handle.block_on(jh);
  println!("[mt] after drop(rt): spawn -> {:?}", r.map_err(|e| e.is_cancelled()));

  // current_thread:只有 Runtime::block_on 能驱动定时器
  let rt = Builder::new_current_thread().enable_all().build().unwrap();
  let handle = rt.handle().clone();
  let start = Instant::now();
  let t = thread::spawn(move || {
    handle.block_on(async {
      tokio::time::sleep(Duration::from_millis(10)).await;
    });
    println!("[ct] other thread: 10ms sleep finished at {:?}", start.elapsed());
  });
  thread::sleep(Duration::from_millis(300));
  println!("[ct] main: 300ms passed, other thread finished = {}", t.is_finished());
  rt.block_on(async {
    tokio::time::sleep(Duration::from_millis(1)).await;
  });
  t.join().unwrap();
  println!("[ct] main: Runtime::block_on returned at {:?}", start.elapsed());
}

输出:

size_of::<Handle>() = 16
[mt] block_on future ran on plain-thread, spawned task ran on tokio-rt-worker
[mt] after drop(rt): spawn -> Err(true)
[ct] main: 300ms passed, other thread finished = false
[ct] other thread: 10ms sleep finished at 306.88275ms
[ct] main: Runtime::block_on returned at 309.395334ms

逐行看:

  • 普通线程上 handle.block_on 的 future 跑在这个普通线程上,handle.spawn 出去的任务跑在 worker 上。
  • drop(rt) 之后 handle.spawn 不 panic,但任务立刻被取消,JoinError::is_cancelled() 为真。
  • current_thread 的 10 毫秒 sleep 等了约 300 毫秒才结束:定时器注册进了时间轮,但没有线程在转时间轮。主线程一调 Runtime::block_on,拿到 Core 去转驱动,时间驱动发现这个定时器早已到期,触发它的 waker,另一个线程才被叫醒。

动脑筋答案

完整程序(src/bin/spawn_before_block_on.rs):

use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use tokio::runtime::Builder;

static RAN: AtomicUsize = AtomicUsize::new(0);

fn spawn_three(rt: &tokio::runtime::Runtime) {
  for _ in 0..3 {
    rt.spawn(async {
      RAN.fetch_add(1, Relaxed);
    });
  }
}

fn main() {
  // A:block_on 一个立刻完成的 future
  let rt = Builder::new_current_thread().build().unwrap();
  spawn_three(&rt);
  rt.block_on(async {});
  println!("A: block_on(async {{}})        -> ran {}", RAN.swap(0, Relaxed));
  drop(rt);

  // B:block_on 里 yield 一次
  let rt = Builder::new_current_thread().build().unwrap();
  spawn_three(&rt);
  rt.block_on(tokio::task::yield_now());
  println!("B: block_on(yield_now())      -> ran {}", RAN.swap(0, Relaxed));
  drop(rt);

  // C:多线程运行时,spawn 后什么也不做,sleep 50ms
  let rt = Builder::new_multi_thread().worker_threads(2).build().unwrap();
  spawn_three(&rt);
  std::thread::sleep(std::time::Duration::from_millis(50));
  println!("C: multi_thread, no block_on  -> ran {}", RAN.swap(0, Relaxed));
}

M2 Max、rustc 1.99.0、--release,连跑 3 次,输出都一样:

A: block_on(async {})        -> ran 0
B: block_on(yield_now())      -> ran 3
C: multi_thread, no block_on  -> ran 3
  • A:一个都没跑。current_thread 的 CoreGuard::block_on 进循环前,waker_ref 先把 woken 置为 true,注释写明是为了让传进来的 future 在进入循环时先被 poll 一次。async {} 第一次 poll 就是 Ready,block_on 直接返回,队列里的 3 个任务没人碰过,随后 drop(rt) 把它们关掉了。
  • B:3 个都跑了。yield_now 第一次 poll 返回 Pending,把 waker 放进 defer 列表。block_on 接着进入「最多跑 event_interval 个任务」的内循环,把队列里的 3 个任务各跑一次。队列空了以后,defer 列表不空,于是走 park_yield,超时为 0 地看一眼驱动,顺手唤醒 defer 里的 waker,回到外循环再 poll 一次 future,这次 Ready。
  • C:3 个都跑了,不需要 block_on。多线程的 build() 在最后一步 launch() 了 worker,worker 自己从注入队列里取任务。这一项依赖 50 毫秒内 worker 已经把任务跑完,3 次运行结果相同。

所以「current_thread 在 block_on 之前没人跑任务」还要再加半句:block_on 的 future 如果一上来就完成了,之前 spawn 的任务也照样没人跑。

小结

  • Builder 是一个普通结构体,默认值都在 Builder::new 里:IO 和时间默认关,max_blocking_threads 512,阻塞线程空闲 10 秒退出,event_interval 61,worker 数取 TOKIO_WORKER_THREADS 或 CPU 数。
  • build() 的顺序固定:驱动、阻塞线程池、调度器,多线程再启动 worker。独占的 Driver 归调度器,共享的 driver::Handle 归句柄。
  • Runtime = Scheduler + Handle + BlockingPool。多线程的 Scheduler 是空结构体,current_thread 的里面放着 Core 和驱动。drop Runtime 会无超时地等阻塞线程退出。
  • Handle 是 scheduler::Handle 枚举,两个变体各包一个 Arc,16 字节,Send + Sync,clone 只是加引用计数。
  • Handle::block_on 不碰调度器:在多线程运行时上和 Runtime::block_on 等价;在 current_thread 上不驱动 IO 和定时器。

💬 关联讨论

发起讨论
加载中...