怎么读这一卷
锁定版本
全卷只对着一个版本写:
| 项 | 值 |
|---|---|
| crate | tokio 1.54.1 |
| tag | tokio-1.54.1 |
| commit | b53b200(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 用户层 API | spawn 全路径、select!、JoinSet、LocalSet |
| 09 设计取舍 | 把各章的取舍汇总到一起 |
| 10 常见坑 | 用报错原文做标题,讲它在源码里怎么产生 |
| 11 在本站中 | 本站(开源)代码里怎么用 tokio |
| 12 动手 | 200 行 mini-tokio 的思路 |