Tokio 源码深度解析

前言:从 .await 到运行时

作者 杨艺韬 · 6,621 字

前言:从 .await 到运行时

"We wanted Tokio to feel like magic — but to reward anyone curious enough to look inside." —— Carl Lerche, Tokio maintainer

写这本书给谁

在展开写作动机之前,先明确一件事:这本书不是写给所有 Rust 开发者的,它是写给一个特定人群的。

这本书是写给你看的,如果你

这本书不适合你,如果你

如果你属于适合的那一群,恭喜——这本书值得你投入。如果不适合,也没关系,挑别的书读。好的技术书都是写给特定人的,不是写给所有人的——后者往往意味着对任何人都不够深。


写作动机:被忽略的那一半

绝大多数 Rust 异步编程教程的结构是一样的:

  1. 解释什么是 async fn
  2. 解释 .await 是挂起当前任务
  3. 教你写几个 tokio::spawn 的小例子
  4. 教你避免在 async 里调 std::thread::sleep
  5. 收工

这讲的是"Future 的语法",不是"运行时的行为"。

如果你读完这些教程,然后在生产环境遇到下面这些问题,大概率是束手无策:

这些问题的答案,全部写在 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 背后的三个问题

让我用一个简短的例子来说明,本书到底要回答什么。

#[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 在一个任务槽里切换,栈空间是怎么复用的?

这三个问题对应本书的三条主线

这本书和市面上其他 Rust 异步书的区别

目前中文世界讲 Rust 异步的内容,基本分三类:

第一类:语法教程 教你写 async fn.awaittokio::spawn。优点是容易上手,缺点是只教"怎么写"、不教"为什么这样写能跑"。读完能做业务,但遇到性能问题或诡异 bug 就抓瞎。

第二类:Future trait 底层机制PinPollWakerContext、状态机展开。这部分是好的,但大多数作者讲到这里就停了——他们讲完了"语言给异步编程提供了什么基础设施",但从未讲"这些基础设施在真实的运行时里是怎么被用起来的"。

第三类:Tokio 使用手册 罗列 Tokio 提供的 API:spawnselect!sleepMutexSemaphore、各种 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 底层的链表结构;sendrecv 的内存序;背压机制;为什么 broadcast 是 ring buffer 而 mpsc 不是

每一章都从"你写的代码"出发,最终落到"Tokio 源码里的具体数据结构和算法"

为什么"读源码"是唯一能真正学会的方式

在讲具体故事之前再留一段思考——为什么这本书的方法论是"读源码"而不是"教你使用"?

因为使用教程有一个根本性的局限:它告诉你"这个 API 应该这么用",但不告诉你"为什么 API 是这个形状"。当你的场景偏离教程的假设时,你就不知道该怎么办——因为教程给的是结论不是推理

源码不一样。源码里写的是 API 的具体实现,你读源码时同时在学**"这个实现选择了什么 tradeoff、为什么选这个"。下次你遇到一个教程没覆盖的场景,你可以从第一性原理推出"应该这么做"**——因为你理解了机制,不是背诵了用法。

这就是为什么本书每一章都贴大量源码(本书估计引用了 tokio-1.40.0 源码约 1500 行)。源码不是装饰,源码是学习的主体。教程和讲解是用来帮你快速看懂源码的辅助。

读的时候重点看什么:不是"这个函数怎么实现"这种字面信息,而是"实现者在这一步面临什么选择、选了哪个、放弃了什么"。每一段好代码背后都是一连串"我可以选 A 但我选了 B、因为..."的决策——把这些决策挖出来,你就学到了写代码的人的判断力。这种判断力比代码本身值钱 10 倍,且不会过时。


本书的阅读路径

全书分六部分,20 章。推荐的阅读方式:

路径 A:完整路径(适合想建立完整心智模型的读者) 第 1 章 → 第 2-3 章(Future 与 Waker)→ 第 4-7 章(Runtime)→ 第 8-10 章(I/O)→ 第 11-13 章(时间与同步)→ 第 14-17 章(高级特性)→ 第 18-20 章(工程实践)

路径 B:问题导向(适合已经在用 Tokio、想解决特定问题的读者)

路径 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 如何利用这个接口实现细粒度的任务调度。读完这两章你对后面的章节会更有感觉。

本书前置知识

本书假设你:

  1. 会写基础的 Rust:所有权、借用、trait、泛型是熟练的;能读懂含 <T: Send + 'static> 这种 bound 的函数签名
  2. 理解 async/await 的基本语义:知道 async fn 返回的是 impl Future.await 意味着挂起;能跑通一个最基本的 tokio::spawn 例子
  3. 了解 Rust 的 PinUnpin:不需要精通,但要知道为什么 async 世界需要 Pin
  4. 有基本的系统编程常识:知道什么是线程、什么是阻塞 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 的 bug,而是使用者对 Tokio 内部机制理解不够导致的误用。

场景一:worker 上的同步阻塞拖垮整个 runtime。服务平时 CPU 30%,每 N 分钟 CPU 飙到 200%、p99 延迟突然恶化——根因往往是某处在 worker 线程上做了同步 I/O 或长 CPU 计算(比如一次大 JSON 反序列化)。修法是把这段代码包进 tokio::task::spawn_blocking。只有懂 Scheduler 的 worker 池和 blocking 池是两套独立池,才能一眼看到这类 bug 的模式(第 5 章、第 16 章)。

场景二:select! 饿死某个分支tokio::select! 有"偏向"和"随机"两种模式,在多消息源的消费者里选错模式,会让某个源看起来"永远没消息"。biased; 修饰符、macro_rules! 的展开顺序、thread_rng_n 的起点随机化——理解了这些机制,debug 这类问题是几分钟的事(第 14 章)。

场景三:Runtime shutdown 精确卡住 60 秒spawn_blocking 起的后台任务(比如阻塞 recv)永远不返回,keep_alive 对"始终 busy"的线程不生效,最终只能靠超时强杀。修法是自行设计 cancellation channel。懂 BlockingPool 的生命周期才能预防这类坑(第 16 章)。

本书的终极目标:让你从"这个 bug 怎么修"升级到"从代码里就看出这会 bug"——因为 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 只是一个极好的训练样本

给两类读者的"阅读前热身"

如果你从下面两类人里对得上,下面的"热身指南"能让你读本书的前几章更顺利。

热身 A:你是"Rust 资深 + 异步新手"

你对 Rust 本身很熟——所有权、泛型、trait、错误处理都是日常工具。但异步部分你基本只写过 async fn + .await,没仔细想过底下是什么。

热身动作

  1. 读《Rust 编译器与运行时揭秘》第 9 章:了解 async fn 是怎么被 MIR 降级成状态机的。30 分钟。
  2. 读《Rust 编译器与运行时揭秘》第 10 章:了解 Pin / Waker / Future trait 的编译期支撑。30 分钟。
  3. cargo expand 看你自己写的 async fn 展开成什么:把一段短代码展开,观察"状态机结构体长什么样"。20 分钟。

做完这些,你读本书第 2 章(Future/poll)和第 3 章(Waker)会极其顺畅——因为你已经有了"编译期视角",本书给你的只是"运行时视角"的另一半。

热身 B:你是"Go / Node 迁 Rust"

你的异步思维已经建立过——goroutine / Promise / event loop——但迁到 Rust 是新的。

热身动作

  1. 读《Rust 编译器与运行时揭秘》第 1 章:建立 Rust 编译管线的整体认识。30 分钟。
  2. 跑一个 Axum hello world(5 分钟)+ 试着把一段 Go goroutine 代码改写成 Tokio(30 分钟)
  3. 读本书第 1 章后,和 Go runtime 的 GMP 对比。差异点会清晰浮现。

做完这些,你会发现 Tokio 和 Go runtime 在调度层面大面积同构——只是设计 tradeoff 不同、依赖不同的语言基础。你已有的并发直觉几乎都能复用。


本书参考资料清单

按权威度从高到低:

S 级(官方源码与官方文档)

A 级(核心维护者公开写作)

B 级(学术原始论文)

C 级(业界公开案例)

所有引用都有出处、都可以查证——发现与源码不符的地方欢迎反馈。


关于本书的体例

最后一些格式约定,让你读起来不被格式问题绊住:

源码片段

行内代码:标识符 / 类型名 / 函数名用 这种字体 标记。保持和源码一致(包括大小写和下划线)。

章节间交叉引用:用 "第 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 月,成都