Rust in Everything
基于 tokio 1.54.1

从 #[tokio::main] 开始

主线位置:任务主线的全程,#[tokio::main] → block_on → spawn → 队列 → poll → 唤醒 → 完成。每一步在后面的章节里展开。

宏展开成什么

最常见的入口:

#[tokio::main]
async fn main() {
  println!("hello");
}

main 不能是 async 的,所以这个属性宏把它改写成一个普通的 fn main。展开逻辑在 tokio-macros/src/entry.rs 的 parse_knobs,生成的代码整理后大致是:

fn main() {
  let body = async {
    println!("hello");
  };
  // 编译期检查 future 的输出类型和函数返回类型一致,运行时不会执行
  let body = {
    if false {
      let _: &dyn ::core::future::Future<Output = ()> = &body;
    }
    body
  };
  {
    use tokio::runtime::Builder;
    return Builder::new_multi_thread()
      .enable_all()
      .build()
      .expect("Failed building the Runtime")
      .block_on(body);
  }
}

函数体原封不动地变成一个 async 块,然后交给新建运行时的 block_on。宏的参数只是往 builder 链上加调用:

写法生成的调用
flavor = "current_thread"Builder::new_current_thread()
flavor = "local"new_current_thread() 加 build_local(...),得到 LocalRuntime
worker_threads = 4.worker_threads(4)
start_paused = true.start_paused(true),测试里冻结时间

所以 #[tokio::main] 没有任何魔法,完全可以手写。需要在进入 async 之前做点同步的初始化(比如读配置决定线程数),就手写这几行。

build() 创建了哪些东西

Builder::build 按 kind 分成两支,多线程走 build_threaded_runtime:

fn build_threaded_runtime(&mut self) -> io::Result<Runtime> {
    // ...
    let worker_threads = self.worker_threads.unwrap_or_else(num_cpus);

    let (driver, driver_handle) = driver::Driver::new(self.get_cfg())?;

    // Create the blocking pool
    let blocking_pool =
        blocking::create_blocking_pool(self, self.max_blocking_threads + worker_threads, worker_threads);
    let blocking_spawner = blocking_pool.spawner().clone();
    // ...
    let (scheduler, handle, launch) = MultiThread::new(
        worker_threads,
        driver,
        driver_handle,
        blocking_spawner,
        // ...
    );

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

    // Spawn the thread pool workers
    let _enter = handle.enter();
    launch.launch();

    Ok(Runtime::from_parts(Scheduler::MultiThread(scheduler), handle, blocking_pool))
}

按顺序是四样东西:

  1. 驱动:Driver::new 建好时间驱动和 IO 驱动(enable_all 就是把 enable_io 和 enable_time 都打开)。
  2. 阻塞线程池:上限是 max_blocking_threads + worker_threads。max_blocking_threads 默认 512,多出来的 worker_threads 个名额是留给 worker 自己的。
  3. 调度器:MultiThread::new 创建每个 worker 的本地队列、共享的注入队列,并把驱动交给它们共用。
  4. 启动 worker:Launch::launch。

第 2 点的那个加法很有意思,看 launch 就明白了:

impl Launch {
    pub(crate) fn launch(mut self) {
        for worker in self.0.drain(..) {
            runtime::spawn_blocking(move || run(worker));
        }
    }
}

worker 线程本身就是阻塞线程池里的线程。 每个 worker 是一个「永远不返回」的阻塞任务,所以线程池上限要先给它们留出位置。这个设计还有一个用处:block_in_place 需要把当前 worker 的工作交给一个新线程,新线程同样从这个池子里来。

block_on 的 future 跑在哪个线程

Runtime::block_on 先进入运行时上下文,再按调度器类型分派。多线程版本在 multi_thread/mod.rs 第 85 行:

pub(crate) fn block_on<F>(&self, handle: &scheduler::Handle, future: F) -> F::Output
where
    F: Future,
{
    crate::runtime::context::enter_runtime(handle, true, |blocking| {
        blocking.block_on(future).expect("failed to park thread")
    })
}

blocking.block_on 最终是 CachedParkThread::block_on:

loop {
    if let Ready(v) = crate::task::coop::budget(|| f.as_mut().poll(&mut cx)) {
        return Ok(v);
    }

    self.park();
}

这是一个最朴素的执行器:poll 一次,没好就让当前线程睡下,等 waker 把它叫醒再 poll。这里的 waker 就是「唤醒这个线程」。

结论:在多线程运行时里,block_on 传进去的 future(也就是 main 的函数体)跑在调用 block_on 的线程上,也就是主线程,而不是 worker。 只有 tokio::spawn 出去的任务才跑在 worker 上。由此可以推出两个常见现象:

  • 在 main 里写一个重计算循环,不会卡住 worker,因为它根本不在 worker 上;但同一个循环放进 tokio::spawn,就会占住一个 worker。
  • current_thread 运行时不一样:它没有别的线程,block_on 所在的线程同时也在跑所有 spawn 的任务(见 current_thread)。
❗IMPORTANT
动脑筋:A 到 D 各打印什么?两个程序,分别运行。第一个:
#[tokio::main]
async fn main() {
  println!("A: {:?}", std::thread::current().name());
  tokio::spawn(async {
    println!("B: {:?}", std::thread::current().name());
  })
  .await
  .unwrap();
}

第二个把属性换成 #[tokio::main(flavor = "current_thread")],两行的标签换成 C 和 D。

提示:Builder 会给 worker 线程起名字。答案在本页末尾。

enter_runtime 还负责检查嵌套。如果当前线程已经在运行时里,它会直接 panic(context/runtime.rs 第 68 行):

Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks. If you are in an async function, use `.await` on the future instead of blocking on it.

这是 tokio 最常见的报错之一,「10 常见坑」里有单独一页:运行时里再起运行时。

tokio::spawn 之后

tokio::spawn 的调用链:

步骤函数做什么
1spawn_inner分配任务 ID;从线程本地上下文取当前运行时的 Handle,取不到就 panic
2scheduler::Handle::spawn按 current_thread / multi_thread 分派
3bind_new_taskOwnedTasks::bind:一次堆分配建好任务,挂进存活任务链表,拿到 JoinHandle 和一个 Notified
4schedule_task决定任务放进哪个队列

第 1 步里「取不到就 panic」,panic 的内容就是另一句常见报错:

there is no reactor running, must be called from the context of a Tokio 1.x runtime

在没有进入运行时的线程上(比如自己 std::thread::spawn 出来的线程)调用 tokio::spawn,就会看到它。

第 4 步是调度器的入口,值得完整看一眼:

pub(super) fn schedule_task(&self, task: Notified, is_yield: bool) {
    // ...
    with_current(|maybe_cx| {
        if let Some(cx) = maybe_cx {
            // Make sure the task is part of the **current** scheduler.
            if self.ptr_eq(&cx.worker.handle) {
                // And the current thread still holds a core
                if let Some(core) = cx.core.borrow_mut().as_mut() {
                    self.schedule_local(core, task, is_yield);
                    return;
                }
            }
        }

        // Otherwise, use the inject queue.
        self.push_remote_task(task);
        self.notify_parked_remote();
    });
}
  • 在 worker 线程上 spawn:走 schedule_local,默认放进当前 worker 的 LIFO 槽,槽里原来的任务挪到本地队列尾部。新任务会是这个 worker 接下来最先跑的。
  • 在 其他线程上 spawn(包括 main 函数体所在的主线程):放进共享的注入队列,再叫醒一个睡着的 worker 来取。

被唤醒的任务也走同一个函数:任务的 Waker 最终调用 Schedule::schedule,也就是 schedule_task(task, false)。所以「新 spawn 的任务」和「被唤醒的任务」对调度器来说是一回事,都是一个 Notified。

worker 的循环

每个 worker 线程执行 run,进入运行时上下文后调 Context::run,主体是一个循环:

while !core.is_shutdown {
    // ...
    // Increment the tick
    core.tick();

    // Run maintenance, if needed
    core = self.maintenance(core);

    // First, check work available to the current worker.
    if let Some(task) = core.next_task(&self.worker) {
        core = match self.run_task(task, core) { /* ... */ };
        continue;
    }

    // There is no more **local** work to process, try to steal work
    // from other workers.
    if let Some(task) = core.steal_work(&self.worker) {
        core = match self.run_task(task, core) { /* ... */ };
    } else {
        // Wait for work
        core = /* ... */ self.park(core);
    }
}

一句话概括:先跑自己的,没有就去偷,偷不到就睡。 next_task 决定先看 LIFO 槽、本地队列还是注入队列;park 时如果抢到了驱动,就去等 IO 和定时器。这三个函数是「04 调度器」的主体。

run_task 把任务交给 Harness::poll,后者根据任务状态决定:真的 poll future、future 完成后写回输出,还是发现任务已被取消直接收尾。这是「03 任务」的主体。

整条任务主线

sequenceDiagram
  participant M as 主线程(block_on)
  participant I as 注入队列
  participant W as worker
  participant T as 任务(Harness)
  participant D as 驱动(IO / 定时器)

  M->>M: Builder::build 建驱动、线程池,启动 worker
  M->>I: tokio::spawn → bind_new_task → push_remote_task
  M->>W: notify_parked_remote 叫醒一个 worker
  W->>I: next_task 取出任务
  W->>T: run_task → Harness::poll
  T-->>D: future 返回 Pending,waker 存进驱动
  W->>D: 没有任务了,park 在驱动上
  D-->>W: 事件就绪,wake → schedule_task
  W->>T: 再次 poll,返回 Ready
  T-->>M: 输出写进任务,唤醒等待的 JoinHandle
  M->>M: block_on 的 future 完成,main 返回

动脑筋答案

M2 Max、rustc 1.99.0、--release,两个程序各跑 3 次,每次输出都一样。第一个:

A: Some("main")
B: Some("tokio-rt-worker")

第二个:

C: Some("main")
D: Some("main")
  • A:main 的函数体是 block_on 的 future,跑在调用 block_on 的主线程上。
  • B:spawn 出去的任务进注入队列,被某个 worker 取走。worker 线程的默认名字在 Builder::new 里设成 "tokio-rt-worker"。1.50.0 之前它叫 tokio-runtime-worker,#7880 把它改短,理由是 Linux 的线程名超过 15 个字符会被截断,原来的名字显示出来只剩 tokio-runtime-w。
  • C:和 A 一样,函数体在主线程上。
  • D:current_thread 没有 worker 线程,spawn 的任务也由 block_on 所在的主线程来跑。

小结

  • #[tokio::main] 就是 Builder::new_multi_thread().enable_all().build().expect(...).block_on(async { 原函数体 })。
  • build() 依次创建驱动、阻塞线程池、调度器,然后把 worker 作为阻塞任务启动起来。
  • block_on 的 future 跑在调用线程上,spawn 的任务才跑在 worker 上。
  • 在 worker 上 spawn 进 LIFO 槽,在外部 spawn 进注入队列;被唤醒的任务也走同一个 schedule_task。
  • worker 循环:先跑自己的,没有就偷,偷不到就 park。

💬 关联讨论

发起讨论
加载中...