Rust in Everything
基于 tokio 1.54.1

怎么读这一卷

锁定版本

全卷只对着一个版本写:

项值
cratetokio 1.54.1
tagtokio-1.54.1
commitb53b200(2026-10-21,chore: prepare Tokio v1.54.1)

1.54.0 在发布两天后被撤回(yanked),所以这一卷用 1.54.1。两者的差别只在定时器(回滚了「从定时器里去掉运行时句柄」这一改动),任务、调度器、IO 的源码完全相同。

源码链接都用 tag,形如 https://github.com/tokio-rs/tokio/blob/tokio-1.54.1/tokio/src/...#L123。master 上的代码每周都在变,用 tag 才能保证链接指向的那一行就是正文讲的那一行。

想在本地对照着读:

git clone https://github.com/tokio-rs/tokio
cd tokio
git checkout tokio-1.54.1

两条主线

tokio 有 10 万多行,平铺着读一定会迷路。全卷围绕两条主线展开,每章开头都会标出本章在主线上的哪一段。

任务主线:一个 future 从交给运行时到拿到结果。

flowchart LR
  A["#[tokio::main]"] --> B[Runtime::block_on]
  B --> C[tokio::spawn]
  C --> D[进入队列]
  D --> E[worker 取出]
  E --> F[Harness::poll]
  F -->|Pending| G[挂起]
  G -->|Waker::wake| D
  F -->|Ready| H[JoinHandle 拿到结果]

IO 主线:任务读 socket 时没有数据,之后怎么被叫醒。

flowchart LR
  A[TcpStream::read] -->|Pending| B[ScheduledIo 记下 waker]
  B --> C[worker 空闲时 park 在 IO 驱动上]
  C --> D[mio 报告可读]
  D --> E[wake]
  E --> F[任务重新被 poll,读到数据]

下面这段程序把两条线都走了一遍,注释标出每一行对应图里的哪个节点:

use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

// 任务主线:建运行时,Runtime::block_on 驱动 main 的 future
#[tokio::main]
async fn main() -> std::io::Result<()> {
  let listener = TcpListener::bind("127.0.0.1:0").await?;
  let addr = listener.local_addr()?;

  // 任务主线:tokio::spawn 把任务放进队列,worker 取出后 poll
  let reader = tokio::spawn(async move {
    let (mut socket, _) = listener.accept().await?;
    let mut buf = [0u8; 5];
    // IO 主线:数据还没到,read 返回 Pending,ScheduledIo 记下 waker,任务挂起
    socket.read_exact(&mut buf).await?;
    Ok::<_, std::io::Error>(buf)
  });

  let mut client = TcpStream::connect(addr).await?;
  // 等 100ms 再写,保证 reader 先等在 read 上
  tokio::time::sleep(Duration::from_millis(100)).await;
  // IO 主线:数据到达,mio 报告可读,wake 把 reader 放回队列,再次 poll 读到数据
  client.write_all(b"hello").await?;

  // 任务主线:reader 返回 Ready,JoinHandle 拿到结果
  let buf = reader.await??;
  println!("{}", String::from_utf8_lossy(&buf));
  Ok(())
}

运行输出 hello(M2 Max、rustc 1.99.0,跑 3 次结果相同)。

两条线在 Waker 那里交汇:IO 驱动、定时器、channel 叫醒任务,用的都是同一套唤醒机制。所以建议先读任务主线(03 任务、04 调度器),再读 IO 主线(05 IO 驱动、06 定时器)。

两种读法

  • 快读:每章只看开头的图和结尾的小结。一个小时能把全卷的骨架过一遍,适合想知道「tokio 大概怎么工作」的读者。
  • 精读:跟着每段代码上方的源码链接,打开原文件对照着看。正文里的代码片段只摘了关键几行,删掉的部分用 // ... 标出,完整上下文以原文件为准。

摘自 tokio 的代码片段一律保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的示例用 2 空格缩进。

前置知识

这一卷默认你已经会写 async Rust,并且知道下面三样东西是什么。不熟的话,先读《Asynchronous Programming in Rust》前几章。

概念这一卷用到它的地方
Future::poll 返回 Poll::Ready / Poll::Pending每个任务最终就是被反复 poll 的一个 future
Waker:Pending 之前把它存起来,事件来了调 wake()任务主线和 IO 主线的交汇点
Pin:future 被 poll 之后不能再移动任务为什么要在堆上一次分配好,再也不挪地方

还有一点会反复出现:Future 本身什么也不做,必须有人来 poll 它。 tokio 的全部工作,就是决定「什么时候、在哪个线程上 poll 哪个 future」,以及「一个 future 返回 Pending 之后,什么时候再 poll 它」。

目录

部分内容
00 导读本部分
01 全景运行时由什么组成;#[tokio::main] 展开后发生了什么;从 tokio-core 到今天的演进
02 运行时骨架Builder、Handle、线程本地上下文、驱动栈
03 任务内存布局、状态机、poll 与唤醒、JoinHandle 与取消
04 调度器本卷重点:本地队列、注入队列、LIFO 槽、任务窃取、协作式预算、阻塞线程池
05 IO 驱动反应器、ScheduledIo、从 TcpStream::read 到挂起
06 定时器分层时间轮、Sleep 的一生
07 同步原语信号量、mpsc、oneshot、watch、broadcast、Notify
08 用户层 APIspawn 全路径、select!、JoinSet、LocalSet
09 设计取舍把各章的取舍汇总到一起
10 常见坑用报错原文做标题,讲它在源码里怎么产生
11 在本站中本站(开源)代码里怎么用 tokio
12 动手200 行 mini-tokio 的思路

💬 关联讨论

发起讨论
加载中...