Appearance
前言:从 .await 到运行时
"A system is understood by those who know what happens in the silences between its explicit actions." —— 笔者
"We wanted Tokio to feel like magic — but to reward anyone curious enough to look inside." —— Carl Lerche, Tokio maintainer
写这本书给谁
在展开写作动机之前,先明确一件事:这本书不是写给所有 Rust 开发者的,它是写给一个特定人群的。
这本书是写给你看的,如果你:
- 用 Tokio 已经有一段时间(>6 个月),能独立写出 async 后端服务
- 在生产 / 真实场景里遇到过 Tokio 的性能或正确性问题,排查时感到束手无策
- 读过 Tokio 官方 tutorial,但感觉"会用但不懂原理"
- 想在 AI 能写代码的时代保持不可替代性,愿意投入数十小时深度读源码
这本书不适合你,如果你:
- 刚开始学 Rust,还没写过 async(先读官方 tutorial)
- 只想快速搭一个服务,不关心内部机制
- 偏好"看视频学习 + 跟敲 demo"的节奏(本书是密度很高的文字书)
- 对底层运行时完全没兴趣(这没问题,但不是本书的读者)
如果你属于适合的那一群,恭喜——这本书值得你投入。如果不适合,也没关系,挑别的书读。好的技术书都是写给特定人的,不是写给所有人的——后者往往意味着对任何人都不够深。
写作动机:被忽略的那一半
绝大多数 Rust 异步编程教程的结构是一样的:
- 解释什么是
async fn - 解释
.await是挂起当前任务 - 教你写几个
tokio::spawn的小例子 - 教你避免在 async 里调
std::thread::sleep - 收工
这讲的是"Future 的语法",不是"运行时的行为"。
如果你读完这些教程,然后在生产环境遇到下面这些问题,大概率是束手无策:
- 为什么我的
tokio::spawn几百个任务,CPU 利用率只有单核的 120%,明明有 16 核? - 为什么一个
.await看起来没有 I/O 的异步函数,偶尔会卡住几十毫秒? - 为什么
select!在某些情况下会偏心某一个分支,导致另一个分支"饿死"? - 为什么
Arc<Mutex<T>>的lock().await和std::sync::Mutex::lock()的性能差异,有时候是 10 倍,有时候反过来? - 为什么
spawn_blocking能让 CPU 密集任务"不阻塞 runtime"——它不也是要占 CPU 的吗? - 为什么
tokio::time::sleep号称"精度很高",但我观察到的精度却在几百微秒到几毫秒之间跳动? - 为什么在 Tokio runtime 之外调用
Handle::current()会 panic? - 为什么
tokio::spawn一个 Future 要求Send + 'static,但tokio::task::spawn_local不要求? tokio::select!宏展开后是一段什么代码?它的"公平性"是怎么实现的?biased;修饰符让它变成什么?- 为什么
tokio::sync::Mutex是公平锁(FIFO)而std::sync::Mutex不是?哪个适合你的场景? JoinHandle::abort()的幕后发生了什么?为什么它"不保证立刻停止"而只是"设置一个 abort 标志"?tokio::task::yield_now()具体做了什么?它和tokio::task::consume_budget的差别在哪?- 为什么 Tokio 用协作式调度而 Go 用抢占式调度?这个差异在什么情况下会咬到你?
这些问题的答案,全部写在 Tokio 的源码里。
而且这些问题的答案互相之间是有结构关联的——理解 Waker 帮你理解 wake 路径,理解 wake 路径帮你理解 Scheduler,理解 Scheduler 帮你理解 work-stealing,每个概念是下一个的垫脚石。本书的结构就是按这个关联组织的——你读完 20 章,会发现这些问题是同一座山脉的不同峰顶,而你已经从基础营地一步一步爬上来。
理解了这些,你对一切生产环境的 Tokio 问题都会有一个基线判断——从"求助于搜索引擎和 Stack Overflow"升级到"我知道从哪里着手排查"。
但 Tokio 的源码不是一份容易啃的代码。它有 10 万行以上,分散在十几个 crate、几十个模块里;大量使用 unsafe、宏、trait 对象、生命周期技巧;核心数据结构(Task、Scheduler、Reactor)层层叠叠、相互引用,初次进入会立刻迷失方向。
这本书的目的,就是带你走完这 10 万行——不是一行一行地读(那样没意义),而是沿着运行时的执行路径,把关键的数据结构、控制流、内存布局、并发同步机制串起来,建立一张完整的地图。
读完这本书,当你再写一行 .await,你会清晰地看到那一刻运行时究竟在做什么。
为什么现在(2026 年)是读这本书最好的时刻
你可能会问:"Rust 异步早就稳定了、Tokio 也成熟多年了,为什么现在才出这样一本书?"答案藏在2020-2026 这六年 Rust 生态的变化里:
2020 年之前:Tokio 还在快速迭代、API 不稳定、社区小。写一本深度剖析书,写完就过时。 2020 年(Tokio 1.0):架构稳定下来,可以开始深度写作。但生态还不丰满,读者基数有限。 2021-2022 年:Hyper、Axum、Tonic、sqlx 等关键库成熟,Rust 后端服务进入大规模生产部署阶段。Discord、AWS、字节跳动、蚂蚁金服等都有重要服务迁到 Rust。 2023-2024 年:Rust 在 AI / 高性能推理 / 数据库 / 区块链等新领域大量应用。读者的深度需求急剧上升——他们不再只想学 API,想理解"Tokio 在生产负载下为什么表现好"。 2025-2026 年:时机刚好。Tokio 架构稳定 5 年、社区成熟、读者需求从"会用"升级到"懂原理"、源码维护已充分、可观测性工具(tokio-console)完整。这本书的所有前置条件都齐了。
更宏观地看——在 AI 可以写代码的时代,会写代码的人会贬值,能理解代码、能做架构决策、能诊断复杂系统问题的人会升值。这本书是给那些想往后者走的人准备的。
读源码的能力是 AI 短期内替代不了的能力——因为它依赖深度模型构建 + 长期记忆这两种人类仍然领先的认知工具。你花几十小时读透 Tokio 源码,在 AI 时代反而是最有杠杆的时间投资。
.await 背后的三个问题
让我用一个简短的例子来说明,本书到底要回答什么。
rust
#[tokio::main]
async fn main() {
let resp = reqwest::get("https://example.com").await.unwrap();
println!("{}", resp.status());
}这段代码在一个 Tokio runtime 中,发起了一个 HTTP 请求,然后打印状态码。它看起来无比简单——但这一行 .await 背后,至少有三个非平凡的问题需要回答:
问题 1:.await 暂停后,控制权去了哪里?
.await 不是 sleep,也不是 yield。它把当前这个 Future 的执行权交还给了某个调度器。那个调度器是谁?它怎么决定下一步运行哪个任务?它是如何记住"这个 Future 等待在哪里、什么时候该叫醒它"的?
问题 2:reqwest::get 里的网络 I/O 是怎么变成"异步"的?
操作系统提供的 socket read/write 本身不是异步的。Linux 的 epoll、macOS 的 kqueue、Windows 的 IOCP 各有各的 API。Tokio 如何把这三种完全不同的系统调用抽象成统一的 .await?当数据到达的时候,内核是怎么告诉用户态"你该醒了"的?
问题 3:一个 Future 在内存里长什么样?
async fn main() 本身就是一个 Future。它内部嵌套的 reqwest::get(...).await 又是一个 Future。嵌套的 Future 在栈上如何安置?Pin 在这里是为什么?多个 Future 在一个任务槽里切换,栈空间是怎么复用的?
这三个问题对应本书的三条主线:
- 第二部分(Runtime 架构)回答"控制权去了哪里"
- 第三部分(I/O Driver)回答"I/O 如何异步化"
- 第一部分(Future 与 poll 模型)+ 前置《Rust 编译器与运行时揭秘》回答"Future 在内存里长什么样"
这本书和市面上其他 Rust 异步书的区别
目前中文世界讲 Rust 异步的内容,基本分三类:
第一类:语法教程 教你写 async fn、.await、tokio::spawn。优点是容易上手,缺点是只教"怎么写"、不教"为什么这样写能跑"。读完能做业务,但遇到性能问题或诡异 bug 就抓瞎。
第二类:Future trait 底层机制 讲 Pin、Poll、Waker、Context、状态机展开。这部分是好的,但大多数作者讲到这里就停了——他们讲完了"语言给异步编程提供了什么基础设施",但从未讲"这些基础设施在真实的运行时里是怎么被用起来的"。
第三类:Tokio 使用手册 罗列 Tokio 提供的 API:spawn、select!、sleep、Mutex、Semaphore、各种 channel。讲用法,配几个 demo,收工。这类内容和 Tokio 官方文档高度重叠,读者读完除了知道"Tokio 有哪些 API",并没有对运行时建立真正的理解。
本书要做的是第四类:从 .await 出发,一路钻进 Tokio 的运行时内部,把每一个关键机制讲透。
具体来说:
| 主题 | 市面上常见讲法 | 本书讲法 |
|---|---|---|
| Waker | "Waker 是用来唤醒 Future 的" | Waker 的 vtable 布局、RawWaker 的 ABI、Tokio 如何把 Waker 和具体的 Task 节点绑定、唤醒到底是怎么传递到 Scheduler 的 |
| Scheduler | "Tokio 有单线程和多线程两种 runtime" | 多线程 runtime 的每个 worker 线程有本地队列 + 全局队列 + LIFO slot;工作窃取的具体算法;为什么选这个算法而不是 Go runtime 那种 |
| Reactor | "Tokio 用 epoll/kqueue 处理 I/O" | tokio::net::TcpStream 注册到 Reactor 的完整流程;一次 read().await 从发出到完成,内核事件如何传递到 Waker,Waker 如何传递到 Task 队列 |
| sleep | "tokio::time::sleep 是异步的" | Tokio 的分层定时器轮(hierarchical timing wheel)结构;为什么不用堆或红黑树;精度与内存占用的权衡 |
| Mutex | "用 tokio::sync::Mutex 避免阻塞" | Tokio Mutex 的排队机制;为什么它不是简单地把 std::Mutex 包了个 async 接口;它的性能特征在什么场景下比 std::Mutex 好 / 坏 |
| channel | "mpsc 是多生产者单消费者" | mpsc 底层的链表结构;send 与 recv 的内存序;背压机制;为什么 broadcast 是 ring buffer 而 mpsc 不是 |
每一章都从"你写的代码"出发,最终落到"Tokio 源码里的具体数据结构和算法"。
为什么"读源码"是唯一能真正学会的方式
在讲具体故事之前再留一段思考——为什么这本书的方法论是"读源码"而不是"教你使用"?
因为使用教程有一个根本性的局限:它告诉你"这个 API 应该这么用",但不告诉你"为什么 API 是这个形状"。当你的场景偏离教程的假设时,你就不知道该怎么办——因为教程给的是结论不是推理。
源码不一样。源码里写的是 API 的具体实现,你读源码时同时在学**"这个实现选择了什么 tradeoff、为什么选这个"。下次你遇到一个教程没覆盖的场景,你可以从第一性原理推出"应该这么做"**——因为你理解了机制,不是背诵了用法。
这就是为什么本书每一章都贴大量源码(本书估计引用了 tokio-1.40.0 源码约 1500 行)。源码不是装饰,源码是学习的主体。教程和讲解是用来帮你快速看懂源码的辅助。
读的时候重点看什么:不是"这个函数怎么实现"这种字面信息,而是"实现者在这一步面临什么选择、选了哪个、放弃了什么"。每一段好代码背后都是一连串"我可以选 A 但我选了 B、因为..."的决策——把这些决策挖出来,你就学到了写代码的人的判断力。这种判断力比代码本身值钱 10 倍,且不会过时。
一个典型的"开窍"场景
笔者写 Tokio 的第三年,遇到过一个生产故障。服务跑在一个 8 核机器上,配置的是 tokio::runtime::Builder::new_multi_thread().worker_threads(8),但有一个接口的 p99 延迟周期性地飙到 500 毫秒——本该几毫秒就能返回的接口。
第一反应:是不是某个 .await 阻塞了?一通 profile,没找到慢点。代码里连一个 std::thread::sleep 都没有,所有 I/O 都用的是 tokio::net,所有同步都用的是 tokio::sync::Mutex。理论上不应该有任何阻塞。
最后用 tokio-console 观察才发现:一个被忽略的计算密集函数,执行时间大约 50ms,在每个 worker 线程上每秒会跑一次。它本身不慢(50ms),但它占用的是 worker 线程。而 Tokio 的 worker 线程数量只有 8 个,8 个线程上这个计算密集函数轮流跑,其他需要被这些 worker 处理的 I/O 任务就排队等待。排到最后的那个请求,就是 p99 飙到 500ms 的那个。
解决方案一行代码:把那个计算密集函数用 tokio::task::spawn_blocking 包起来,让它去跑在专门的阻塞线程池里,把 worker 线程腾出来处理 I/O。
这个问题如果只会"用 Tokio",永远定位不到——你会以为 Tokio 的调度有 bug。但如果理解 Tokio 的 Scheduler 结构(worker 线程 + blocking 线程池是两套独立的池),这个问题几乎是一眼看到的。
这就是"懂内部"和"会使用"之间的价值差距。
本书的阅读路径
全书分六部分,20 章。推荐的阅读方式:
路径 A:完整路径(适合想建立完整心智模型的读者) 第 1 章 → 第 2-3 章(Future 与 Waker)→ 第 4-7 章(Runtime)→ 第 8-10 章(I/O)→ 第 11-13 章(时间与同步)→ 第 14-17 章(高级特性)→ 第 18-20 章(工程实践)
路径 B:问题导向(适合已经在用 Tokio、想解决特定问题的读者)
- 生产环境诡异延迟 → 第 5 章(Scheduler)+ 第 16 章(blocking)+ 第 19 章(性能调优)
- I/O 性能优化 → 第 8-10 章 + 第 19 章
- 异步同步原语选型 → 第 12-13 章
- 复杂并发模式 → 第 14-15 章 + 第 18 章
路径 C:源码贡献者路径(适合准备向 Tokio 或周边生态提交 PR 的读者) 第 4 章 → 第 6 章(Task 结构) → 第 5 章(Scheduler) → 对应你要改动的模块章节
路径 D:对标 Go runtime 的读者 第 4-6 章重点看 Tokio 的调度模型,和 Go 的 GMP 做对比;第 8-10 章看 Tokio 的 Reactor,和 Go 的 netpoller 做对比;第 11 章看 Tokio 的定时器轮,和 Go runtime 的 timer 做对比。
建议:不要跳过第 2-3 章。即使你"很熟悉 Future 和 Waker",本书对这两个概念的讲法和绝大多数教程不一样——本书关注的是 Waker 作为运行时与 Future 的接口这个角色,以及 Tokio 如何利用这个接口实现细粒度的任务调度。读完这两章你对后面的章节会更有感觉。
本书前置知识
本书假设你:
- 会写基础的 Rust:所有权、借用、trait、泛型是熟练的;能读懂含
<T: Send + 'static>这种 bound 的函数签名 - 理解 async/await 的基本语义:知道
async fn返回的是impl Future,.await意味着挂起;能跑通一个最基本的tokio::spawn例子 - 了解 Rust 的
Pin和Unpin:不需要精通,但要知道为什么 async 世界需要Pin - 有基本的系统编程常识:知道什么是线程、什么是阻塞 I/O 与非阻塞 I/O、大致知道
epoll是干什么的(不需要用过)
如果上面 1-3 你觉得不熟,笔者强烈建议你先读《Rust 编译器与运行时揭秘》的第 9 章(async/await 状态机)和第 10 章(Pin、Waker、Future)。那本书从编译器的角度讲清楚了"async fn 被展开成了什么",本书在此之上讲"展开之后怎么跑起来"。两本书是语言层 + 运行时层的互补关系。
如果第 4 条你没概念,任何一本《Linux 网络编程》或《Unix 环境高级编程》的前几章都能补齐;本书第 9 章也会对 epoll / kqueue / IOCP 做足够的回顾。
本书的学习节奏建议
不同读者有不同的最佳阅读节奏。这里提供几种常见的:
节奏 A:连贯 2-3 周读完 适合有大段时间、想一次吃透的读者。每天读一章、做笔记、跑代码例子。20 章大约 20-30 天读完。建议周末把 Tokio 源码 clone 下来,边读边查源码对照。
节奏 B:按需查阅 + 系统读核心 更务实的节奏。完整读第 1-6 章(核心机制),第 7-17 章按需跳读——遇到生产问题时查对应章节。第 18-20 章(工程实践)通读。总时长大约 1-2 个月。
节奏 C:边做项目边读 你正在写一个 Tokio 相关的项目,本书作为"延伸参考资料"。每次你在项目里遇到一个感兴趣的点("为什么 select! 要偏向?"、"spawn_blocking 到底怎么 drop?"),翻对应章节读一节。项目做完、书也读完。这是效率最高的方式,因为每一页都有现实问题锚定。
节奏 D:和同事组读书会 团队级学习。每周一次 90 分钟,两个人轮流主讲一章,其他人提问。这种读法的好处:同事的提问会暴露你自己没想到的盲点。6-8 周可以读完核心章节。
不管选哪种节奏,避免"一口气囫囵吞枣读完"——这种方式对深度技术书几乎没有留存。慢读、手画图、写笔记,你会发现同一段源码第二次读比第一次理解深很多。
源码约定
本书源码引用遵循以下约定:
- 版本锚定:所有源码路径都对应 Tokio 1.40 stable(2026 年 4 月发布)。后续版本可能重构,但本书的分析方法学(如何定位模块、如何追踪数据流)不会过时
- 路径标注:每段核心源码的引用都会标注
tokio/src/runtime/scheduler/multi_thread/worker.rs:123这样的精确位置 - 节选原则:源码节选只保留与当前讨论相关的行,省略的部分用
// ...标注;完整代码请参考 GitHub 上对应 tag - 注释说明:行内注释会用
// ← 关键:...这种醒目的前缀标注本书新增的解释
三段真实的生产故事
我在前面提过"用 Tokio 六年间遇到过数十个性能问题"。为了让你对这本书要教你的能力类型有具体感,下面讲三段真实故事——都是真实发生过的,细节已改以保护隐私。
故事一:服务每 5 分钟一次 CPU 抖动到 200%
现象:一个跑 gRPC 的后端服务,平时 CPU 使用率稳定在 30% 左右。但每 5 分钟左右会有一次 10 秒窗口里 CPU 飙到 200%(双核机器)。这个抖动影响 p99 延迟。
排查过程:
- 先用 perf 看火焰图——没发现明显热点,CPU 时间均匀分布在各种函数上
- 用 tokio-console 观察 worker busy time——抖动期间所有 worker 都 100% busy
- 然后用
eBPF追踪epoll_wait调用频率——发现抖动期间 epoll_wait 调用频率从 500/s 掉到 50/s - 最终定位:一个定时任务
每 5 分钟执行一次 Redis 刷新,Redis 返回了 50 MB 的单次响应,反序列化消耗 10 秒——在 worker 线程上同步反序列化 - 修法:把反序列化放进
tokio::task::spawn_blocking——一行代码,抖动消失
教训:worker 线程上的同步阻塞会蔓延到整个 runtime。这个 bug 诞生的根源不是 Tokio 设计问题,而是开发者不理解"worker 线程不能被阻塞"。本书第 5 章(Scheduler)和第 16 章(spawn_blocking)会让你一眼看到这类 bug 的模式。
故事二:select! 让一个 channel 饿死了
现象:一个用 tokio::select! 处理多个消息源的消费者,一个源的消息消费速度明显慢于另一个源——即使后者的消息发送得比前者多得多。
排查过程:
- 一开始以为是 channel 实现有问题——换了 crate,没用
- 仔细看
select!宏展开——发现它默认是按代码中声明顺序 poll 分支 - 第一个分支恰好总是 Ready(因为它的源发送频率高),select! 永远从第一个分支取消息
- 第二个分支的 channel 虽然有消息,但永远没被 poll 到
修法:加 biased; 关键字启用"偏向模式"(显式按顺序)、或用 tokio::select! 的公平模式(默认每次随机选分支)。我们最终用了公平模式——瞬间 fix。
教训:select! 宏的公平性不是直觉的。本书第 14 章(select! 宏展开)会把这个宏的展开代码和公平性逻辑完整拆给你看。
故事三:Runtime shutdown 卡住了 60 秒
现象:服务收到 SIGTERM 后应该 30 秒内优雅关闭。但实际需要 60 秒——精确地、每次都是 60 秒。
排查过程:
- 60 秒这个数字很可疑——不像是任意的卡住
- 查 Tokio 文档:
max_blocking_threads的keep_alive默认是 10 秒、但某些 spawn_blocking 任务可能被看作"长时运行",等长达 60 秒才回收 - 实际原因:我们用
spawn_blocking起了一个后台任务调用blocking_recv——它一直 block 等消息、永远不返回——keep_alive 对这种"始终 busy" 的线程不生效,它只在"线程空闲 N 秒后"回收 - 进程 exit 要等所有 daemon blocking 线程退出——我们那个任务永远不退出,系统等 60 秒超时强杀
修法:给 blocking 任务一个 cancellation channel,shutdown 时发信号让它主动退出。
教训:spawn_blocking 的生命周期管理要自己负责。Tokio 只管"起线程跑",它不管"怎么停"。本书第 16 章(blocking)会把 BlockingPool 的完整生命周期讲透。
这三个故事的共性
三个 bug 都不是 Tokio 的 bug——它们都是开发者对 Tokio 内部机制理解不够导致的误用。Tokio 是一个工具,工具能造出奇迹也能造成灾难,区别只在使用者理解它的深度。
本书的终极目标是:让你从"这个 bug 怎么修"升级到"我从代码里就看出这会 bug"——因为你把 Tokio 的内部机制内化了、知道哪些模式是陷阱。这种能力能让你在生产故障的第一小时就定位根因,而不是花 3 天排查。
关于作者
笔者 2018 年起接触 Rust,2020 年起在生产环境大规模使用 Tokio。过去六年间:
- 设计并维护过多个基于 Tokio 的后端服务(网关、消息队列、AI Agent 运行时)
- 阅读 Tokio 源码的总时长估计在 500 小时以上,贡献过若干小补丁
- 在生产环境定位过数十个与 Tokio 调度、
spawn_blocking误用、.await陷阱相关的性能问题 - 给团队做过多次 Tokio 内部机制的内训
本书是这六年理解的结晶。不会教你 Tokio 的 API 该怎么用(官方文档够了),但会教你为什么 Tokio 是现在这个样子、为什么这个样子是对的——以及在哪些情况下它还不够对、你应该怎么绕过它。
本书不会教你的东西
为了让你在翻开第 1 章前就对得到和得不到什么有清晰预期,本书明确不讲以下内容:
不讲 Tokio 的 API 使用教程tokio::spawn 怎么用、async fn 怎么写、#[tokio::main] 的基本用法——官方的 Tokio Tutorial 讲得清楚、有官方权威度。本书不和它竞争,本书是它的下一步。
不讲 Rust 异步语法基础 如果你不知道 async fn 为什么返回 impl Future、不知道 .await 是什么意思、没写过一个 tokio::spawn,先去读官方 tutorial 或者《Rust 编译器与运行时揭秘》第 9-10 章再回来。
不讲 Axum / Actix / Hyper 怎么用 虽然这些是 Tokio 生态的上层,但它们每个都值得单独一本书。本书聚焦运行时本身。
不讲"怎么选运行时" 第 1 章会给你一份对比表,但一旦选定 Tokio,本书就全力讲 Tokio——不会在每一章都"但是 smol / monoio 是这样做的"。偶尔出现的跨运行时对比是为了帮你看清 Tokio 的独特之处,不是让你犹豫。
不讲生产环境运维 部署、监控、日志聚合、压测工具——这些是另一个话题。本书给你"怎么从运行时层面诊断问题"的能力,但不教 DevOps。
不讲 Rust 初级语法 所有权、生命周期、trait、泛型的基本使用默认已会。会讲更深的地方——比如 Pin<&mut Self> 对 Future trait 的意义、Arc<Task> 引用计数和 state 位如何共用一个 atomic——但基础假设你已经会。
一些可能的反对意见
作为一个有立场的作者,我应该诚实面对读者可能提出的反对意见:
反对一:"你讲得太深,我只是想用 Tokio 写个服务" 如果你只是想用,你不需要这本书——你需要的是官方 tutorial 和 Stack Overflow。但如果你已经用 Tokio 2 年还是觉得它像黑盒,这本书就是为你写的。
反对二:"Tokio 源码本身免费开源,自己读不就行了?" 理论上是。实践上 Tokio 源码 10 万行,没有导读你会在 harness.rs / state.rs / raw.rs 之间迷路。本书的价值是给你一张地图和一条路径——源码还是要你自己读的,只是不用从零开始找方向。
反对三:"AI 时代了还需要读源码吗?" 需要。AI 可以帮你生成代码、回答具体问题,但理解一个复杂系统的架构决策需要你在脑子里跑模拟——这是 AI 目前给不了你的。而且越会用 AI 的人越需要深度——AI 能替代的都是浅层工作,"能理解别人十年写的代码"这件事在 AI 时代价值不降反升。
反对四:"Tokio 每年都在改,今天读的细节明年就过时了?" 部分对。源码细节(某个函数的具体行号)会变,但架构决策不怎么变。Tokio 从 1.0 到 1.40(2020-2026)的架构几乎没动过,变的只是性能优化和新特性。本书的价值是架构理解,不是源码字典——这种理解能陪你走过 Tokio 的下一个五年。
反对五:"我做的不是 Rust 后端,读这个有什么用?" Tokio 用到的很多设计在其他地方也有——Go runtime、Java ForkJoinPool、JavaScript microtask queue、Vue 响应式调度——你读完 Tokio 源码,再看其他运行时代码会感到熟悉。好的源码阅读能力是可以跨技术栈复用的,Tokio 只是一个极好的训练样本。
一个可能没人告诉你的事实
作为软件工程师,你的知识深度不是由你写过多少代码决定的,是由你读过多少代码决定的。写代码让你学会一种工具,读代码让你学会一种思维方式。
写 10 年业务代码的工程师和读 10 年源码的工程师,差距不是 2 倍或 3 倍,是 5-10 倍的"看问题的能力"。前者能写出工作的代码,后者能判断一段代码会不会工作、为什么工作、在什么条件下失败。
本书希望把你往"读源码"那条路上推一把。读 Tokio 源码的过程中,你会积累一套思维工具:位打包状态机、无锁数据结构、vtable ABI、状态位 + 引用计数合一、work-stealing 随机起点——这些工具会伴随你的整个职业生涯,远超过 Tokio 本身。
致谢
感谢 Tokio 的核心维护者团队(特别是 Carl Lerche 和 Alice Ryhl),他们用六年时间把一个粗糙的异步原型打磨成了今天这个稳定、可扩展、可观测的工业级运行时,并且全部开源。
感谢在 yyt-jt 社区给本书早期章节提过反馈的读者。这本书的很多"开窍场景"最初是来自真实读者提出的生产环境问题——没有这些问题,就没有现在的这本书。
最后,感谢所有愿意花时间读源码的工程师。在 AI 能生成代码的时代,愿意花十小时读懂别人十年写成的代码的人,在未来会变得比过去任何时候都稀缺。
给两类读者的"阅读前热身"
如果你从下面两类人里对得上,下面的"热身指南"能让你读本书的前几章更顺利。
热身 A:你是"Rust 资深 + 异步新手"
你对 Rust 本身很熟——所有权、泛型、trait、错误处理都是日常工具。但异步部分你基本只写过 async fn + .await,没仔细想过底下是什么。
热身动作:
- 读《Rust 编译器与运行时揭秘》第 9 章:了解
async fn是怎么被 MIR 降级成状态机的。30 分钟。 - 读《Rust 编译器与运行时揭秘》第 10 章:了解 Pin / Waker / Future trait 的编译期支撑。30 分钟。
- 跑
cargo expand看你自己写的async fn展开成什么:把一段短代码展开,观察"状态机结构体长什么样"。20 分钟。
做完这些,你读本书第 2 章(Future/poll)和第 3 章(Waker)会极其顺畅——因为你已经有了"编译期视角",本书给你的只是"运行时视角"的另一半。
热身 B:你是"Go / Node 迁 Rust"
你的异步思维已经建立过——goroutine / Promise / event loop——但迁到 Rust 是新的。
热身动作:
- 读《Rust 编译器与运行时揭秘》第 1 章:建立 Rust 编译管线的整体认识。30 分钟。
- 跑一个 Axum hello world(5 分钟)+ 试着把一段 Go goroutine 代码改写成 Tokio(30 分钟)
- 读本书第 1 章后,和 Go runtime 的 GMP 对比。差异点会清晰浮现。
做完这些,你会发现 Tokio 和 Go runtime 在调度层面 80% 同构——只是设计 tradeoff 不同、依赖不同的语言基础。你已有的并发直觉几乎都能复用。
本书参考资料清单
本书引用的材料按权威度从高到低:
S 级(官方源码与官方文档):
rust-lang/rustGitHub repo —— 所有 core、std 层面引用tokio-rs/tokioGitHub repo —— 本书核心材料来源- tokio.rs 官方文档和博客
A 级(核心维护者公开写作):
- Carl Lerche、Alice Ryhl、Sean McArthur 的 RFC、博客、GitHub 讨论
- "Making the Tokio scheduler 10x faster"(Tokio 官方博客)
rust-lang/rust的 issues,特别是#66281(will_wake vtable 问题)
B 级(学术原始论文):
- Chase & Lev 的"Dynamic Circular Work-Stealing Deque"(2005)
- Blumofe 等的 Cilk 论文(1995)
C 级(经验与推断):
- 本书作者六年的生产经验
- 业界公开案例(Discord、AWS、字节跳动等公开分享)
所有引用都有出处、都可以查证。本书尽最大努力做到信息可追溯——如果你发现任何与源码不符的地方,请告诉我,我会改正。真实性是这本书对你做的第一承诺。
关于本书的体例
最后一些格式约定,让你读起来不被格式问题绊住:
源码片段:
- 带
// 来源:...注释的源码都是原样从 GitHub 拉取,仅删除了与讨论无关的属性注解和长篇 doc comment - 带"简化自 ..."标注的源码是为了便于理解做了简化,保留语义但删了边界情况处理
- 带"伪代码示意"的源码是我编的——只是结构示意,不是真实 Tokio 代码。这种情况会明确说明
行内代码:标识符 / 类型名 / 函数名用 这种字体 标记。保持和源码一致(包括大小写和下划线)。
章节间交叉引用:用 "第 N 章(标题)" 的形式。你可以 Ctrl+F 搜章号跳转;在 web 阅读版本里是超链接。
对其他书的引用:《Rust 编译器与运行时揭秘》第 N 章、《Vue 3 设计与实现》第 M 章——这两本是同一作者的配套读物,点击链接可以跳到在线版本。
mermaid 图:本书用 mermaid 画了不少架构图、调用路径图。在 web 版本里都是可交互的(可缩放);在 PDF / 印刷版本里是静态图。
行号引用:比如"tokio/src/runtime/scheduler/multi_thread/worker.rs:123"——这种精确位置引用对应 tokio-1.40.0 tag 时的行号。后续版本如果文件重构,行号会漂移,但函数名通常不会变——用函数名找更可靠。
快捷键提示:阅读 web 版本时,键盘 ← → 可以在章节间跳转,[ ] 可以切章节侧栏。
最后一件事:本书是开源的(CC BY-NC 4.0)。如果你发现错误、有更好的讲解思路、想补充真实案例,欢迎通过 GitHub issue 或微信(关注"杨艺韬讲堂"公众号)联系我。每一个读者反馈都会让下一版更好——这本书的长期生命力来自读者。
让我们翻开第一章。
——杨艺韬 2026 年 4 月,成都