Tokio 源码深度解析

第1章 Tokio 在 Rust 异步生态中的位置

作者 杨艺韬 · 9,408 字

第1章 Tokio 在 Rust 异步生态中的位置

本章要点

  • Rust 异步模型的一个独特选择:语言定义 Future trait,运行时留给生态
  • Tokio 从 2016 年的 mio 一路演化到 2026 年 1.40 版本的关键技术节点
  • Tokio 与 smol、async-std、monoio、glommio 的真实差异(不是"哪个更好",而是"哪个场景用哪个")
  • Tokio 的顶层架构:Runtime、Scheduler、Driver、Task 四大子系统,以及 Waker 这个贯穿始终的"神经"
  • 一次 .await 在这张架构图上的完整走位
  • 本书后续 20 章如何对应这张地图的每个区域

1.1 一个反直觉的选择:Rust 把运行时踢出了语言

写过 Go 的人会习惯一件事:你写 go f(),就有一个 goroutine 被调度、被栈管理、被 GMP 分派;你写 time.Sleep(...),就有一个 timer 被 runtime 管理;你写 conn.Read(...),底下是 netpoller 在用 epoll/kqueue 等你的数据。

所有这些"运行时基础设施"都藏在了 Go 的语言运行时里。你不需要导入任何库,gochanselect 是 Go 关键字,runtime 是标准库的一部分。这给 Go 带来了无与伦比的易用性,也带来了"所有 Go 程序都带着一个同款运行时"的代价——当你想在嵌入式设备上跑 Go、或者想让 Go 程序不自带 GC,你就会撞墙。

Rust 做了一个反过来的选择。Rust 语言只给了你两样东西:

  1. Future trait(定义在 core::future 中):一个可以被 .poll() 的状态机抽象
  2. async/await 语法糖:编译器把 async fn 展开成实现了 Future trait 的匿名状态机类型

语言不给你调度器、不给你 I/O 多路复用、不给你定时器、不给你 channel。这些全部留给第三方 crate(也就是"运行时")去实现。

这个选择在一开始被很多人批评为"增加了心智负担"、"碎片化"。但它同时换来了几个非平凡的好处:

代价也是真实的:

Tokio 就是在这个"把运行时留给生态"的空位上长出来的。而且它长到了一个地位:绝大多数 Rust 后端代码都在 Tokio 上跑。本书的任务是把这个"绝大多数人都在用、但极少数人真正理解"的运行时讲透。

Rust 这个决策的代价和红利一起清算

我们已经看到了代价——生态碎片化的风险、陡峭的学习曲线、运行时兼容性问题。那红利是什么

红利一:Rust 可以在没有堆的环境里跑异步代码 STM32、RP2040、ESP32 这类 MCU 没有 malloc。Python / JS / Go 的异步模型都依赖堆分配——Rust 不依赖embassy 运行时让你在 32 KB RAM 的单片机上跑 async Rust——这是其他语言做不到的。

红利二:WebAssembly 场景有原生支持 WASM 没有线程(大部分情况下)、没有传统 epoll,只有 event loop。Rust 的 Future 模型天然适应这种环境——wasm-bindgen-futures 把 Rust Future 桥接到 JS 的 Promise / event loop,代码可以在浏览器和服务端共用同一套 async 代码

红利三:同一套 async 代码可以跑在完全不同的运行时上 如果你的库用 AsyncRead / AsyncWrite trait 而不是 Tokio 具体类型,它可以被 Tokio、smol、monoio 用户使用。好的库作者会尽量泛化运行时,保留灵活性。坏的库作者会绑死 Tokio(当然大部分时候这也没问题)。

红利四:运行时本身是可演化的 JavaScript 的 event loop 模型 20 年没变过。Tokio 调度器每两三年大改一次(见第 5 章)。语言不绑运行时,意味着运行时可以快速进化

一句话:Rust 把运行时留给生态这个选择,是长期思维——短期看它付出了 7 年才成熟的代价,但换来的是十年、二十年维度的灵活性。这种"牺牲短期换长期"的工程决策,正是 Rust 设计哲学里最珍贵的部分。

1.2 Tokio 的前世:从 mio 到 1.40 的十年

一个项目的历史往往比它的当前状态更能说明问题——Tokio 从 2016 年一个基于 mio 的实验性 epoll 封装、到 2020 年 1.0 正式发布、到 2026 年成为 Rust 异步生态事实标准,十年时间经历了至少三次重大架构重构。每一次重构都在回答"Rust 异步应该是什么样"的问题。理解这段历史能让你对现在的 Tokio 有一种"来之不易"的体会——它的每一个设计决策背后都有数年的实践验证。

要理解 Tokio 为什么是现在这个样子,得看它是从什么演化过来的。这不是掉书袋——很多 Tokio 当下的设计决策,只有知道它的历史才能理解"为什么不是另一种做法"。

2016:mio —— 一切的起点

Tokio 还不存在时,Carl Lerche 先写了 mio(Metal I/O)。mio 做一件事:把 Linux 的 epoll、macOS/BSD 的 kqueue、Windows 的 IOCP 抽象成一套统一的跨平台非阻塞 I/O API

mio 本身没有"Future"、没有"async"的概念,它的 API 长这样:

// 伪代码,示意 mio 早期的 API 形态
let poll = Poll::new()?;
let listener = TcpListener::bind("127.0.0.1:8080")?;
poll.registry().register(&listener, Token(0), Interest::READABLE)?;

let mut events = Events::with_capacity(128);
loop {
    poll.poll(&mut events, None)?;
    for event in &events {
        match event.token() {
            Token(0) => { /* accept a new connection */ }
            _ => { /* handle other events */ }
        }
    }
}

mio 是基础层。它的作用类似于 Node.js 的 libuv、Go runtime 的 netpoller:把操作系统级的事件通知机制统一成一套 API。

直到今天,Tokio 内部的 I/O Driver 依然建立在 mio 之上。你会在本书第 9 章看到 mio 的具体源码。

2016-2018:futures 0.1 + tokio 0.1 —— 基于 poll 函数的第一代

2016 年底,Aaron Turon、Alex Crichton 在 mio 之上提出了 futures crate——也就是 Future trait 的第一个版本:

// futures 0.1 的 Future trait(简化)
pub trait Future {
    type Item;
    type Error;
    fn poll(&mut self) -> Poll<Self::Item, Self::Error>;
}

注意这个版本:没有 Pin、没有 Waker、没有 Context。唤醒机制是通过 thread-local 存储的"当前任务句柄"实现的。这个设计简单,但有一些问题(比如不好实现自引用状态机、跨线程唤醒需要 wrapper)。

Tokio 0.1 建立在 futures 0.1 之上,提供了第一个可用的 Rust 异步运行时。但那个年代的 Rust 异步写起来很痛苦:

// Tokio 0.1 时代的写法(无 async/await)
let task = socket
    .read_to_end(buf)
    .and_then(|(socket, buf)| {
        process(&buf).into_future()
    })
    .map(|result| { /* ... */ })
    .map_err(|e| { /* ... */ });
tokio::spawn(task);

这种"and_then / map / map_err 链"的写法被称为 "combinator 风格"。它能跑,但对初学者极其不友好,错误类型传递复杂,嵌套 Future 的时候更是一场噩梦。

2019:std::future::Future + async/await 语言级支持

2019 年是分水岭。Rust 1.36 把 Future trait 稳定到了 std::future 模块,Rust 1.39 把 async/await 语法稳定。新的 Future trait 长这样:

// std::future::Future(今天依然是这个样子)
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

三个关键变化:

这三个变化让 Tokio 有了重生的机会。但它也意味着 Tokio 0.1 的大量代码和用户代码需要重写。

2020:Tokio 1.0 —— 工业级稳定

2020 年 12 月,Tokio 1.0 发布。这是第一个做出稳定性承诺的版本:Tokio 1.x 不会再有 breaking change。这个承诺到今天(2026 年)依然有效,Tokio 1.40 依然在 1.x 系列里。

Tokio 1.0 的核心设计在今天依然有效:

2020-2026:持续精进

Tokio 1.0 之后的 5 年多,主干没有大改动,但有许多关键的增量改进:

Tokio 1.40(2026 年 4 月) 是本书的分析锚点。它的架构在 1.0 的骨架上增量演化,理解了 1.0 的设计,后续版本对你不再陌生

1.2½ 为什么 Rust 异步比其他语言成熟得慢

读完 1.2 节的演化史,你可能会注意到一个反常现象:JavaScript 在 2011 年就有了 Promise,Python 3.5 在 2015 年稳定了 async/await,Rust 却等到 2019 年——而且从 2019 的 async/await 稳定到 2020 的 Tokio 1.0 还花了一年多,整个异步生态完全"生产可用"实际上要到 2022 年。Rust 异步的成熟速度比其他主流语言慢 5-7 年,这不是因为 Rust 社区不努力,而是因为 Rust 在异步这件事上刻意选择了最难的路

几个关键原因:

原因一:零成本抽象的硬约束 JavaScript 的 Promise 是个堆分配的对象,里面有回调链、微任务队列等等。Python 的 coroutine 是个有栈的 generator。这些方案都不符合 Rust 的"零成本抽象"原则——Rust 不允许 async 引入任何"必不可少的运行时开销",包括 GC、绿色线程栈、强制堆分配。满足这个约束的唯一路径是把 async 编译成一个栈上状态机——这个 idea 直到 2017 年的 generators RFC 才被系统性论证,之后还需要编译器的大量 MIR 工作。

原因二:Pin 难题 上面状态机方案会自然产生自引用结构体(状态里既有 buffer 又有 &buffer)。自引用和 Rust 的 move 语义冲突——move 一个自引用结构会让内部指针悬垂。解决这个需要发明 Pin 这个新原语——而 Pin 本身的 API 设计被讨论了 2 年才稳定,期间反复 RFC。《Rust 编译器与运行时揭秘》第 10 章详拆了 Pin 的完整语义。

原因三:trait 系统的缺环 2015-2017 的 Rust 类型系统里缺了几个支撑异步 trait 的关键特性:

Rust 社区不愿意在这些基础不牢的情况下稳定 async trait——代价是等待,但换来的是长期的 API 稳定。

原因四:Rust 的民主化开发模式 Rust 所有重要决策都走 RFC 流程,几十人到几百人讨论数月。JavaScript / Python 没有这种负担——BDFL 或少数核心团队拍板。民主化慢但稳,最终决策质量更高。

总结:Rust 异步是业界最野心勃勃的异步模型——要零成本、要类型安全、要编译期验证。这些目标之间相互制约,每个都需要语言和生态多年打磨。今天你写一行 .await 能跑起来、能跨线程、能零开销,是 2015-2022 这七年社区耐心的结果。Tokio 作为这个生态的主运行时,每一个设计决策都浸泡在这段历史里——读它的源码时带着这份历史感,你会看到更多东西。


1.3 Rust 异步运行时的横向对比

"为什么是 Tokio 而不是别的"——这个问题值得用整整一节来回答。Rust 异步生态有多个运行时选项(Tokio、async-std、smol、embassy、monoio、glommio),它们各自占据不同的应用场景。理解这个图谱不只是"选型知识",更是理解"Tokio 的设计取舍"——看清了别的运行时在什么维度上偏向不同的选择,你才能看懂 Tokio 为什么选了这条路。这一节把这些候选一一讲清,帮你建立"Rust 异步生态全景"的完整视角。

读到这里你可能会问:既然语言把运行时留给生态,那除了 Tokio,还有什么?为什么 Tokio 最终占了主导?其他运行时在什么场景下有优势?

下面这张表梳理了 2026 年 Rust 异步生态的主要运行时:

运行时 定位 核心差异点 典型场景
Tokio 通用、工业级 多线程 work-stealing + epoll 兼容所有平台 + 大而全的生态 90% 的 Rust 后端
smol 极简、模块化 核心只有几千行,executor/reactor/timer 可独立使用 需要自定义运行时组合的场景、学习用
async-std 模拟 std API API 形状刻意贴近 std,async_std::fs 对标 std::fs 已经不活跃(2024 年起 maintenance 模式)
monoio thread-per-core + io_uring 每个线程一个独立的 executor + reactor,不做工作窃取;强绑 io_uring 字节跳动内部的高性能 RPC/存储服务
glommio thread-per-core + io_uring 和 monoio 类似思路,Datadog 主推;调度更偏向任务优先级 延迟敏感的数据库、Scylla 等
embassy 嵌入式、no_std 无需堆、无需线程、直接用 interrupt 作为 reactor STM32、RP2040 等单片机
pollster 最小化阻塞 executor 几十行,给单 Future 用 block_on 不需要运行时的场景(脚本、工具)

每一个运行时的深层定位

Tokio 的定位:通用后端。作者 Carl Lerche 2016 年开始写,现在是 Discord、AWS、Deno、Linkerd、字节跳动 Volo、Meta(部分服务)的底层。Tokio 代表 Rust 异步生态的"主流答案"——不求最极端的性能,但要最稳定的生产体验。

smol 的定位:教育 + 模块化。作者 Stjepan Glavina 是 async-std 早期核心贡献者,后来觉得 async-std 太重,做了 smol 这个精简版。smol 的代码非常可读——大约 3000 行核心代码,推荐所有想理解运行时原理的人读一遍 smol 源码作为 Tokio 的"轻量对照组"。

async-std 的定位(已 deprecated):2019 年试图做"异步版 std"——API 刻意和 std 对齐,让 std::fs::File::openasync_std::fs::File::open 只差一个 .await。理念好但失败了:维护团队在 2023 年明确 deprecate 了,主要因为:

一本书很少讲 failed project,但 async-std 的失败有教益:生态战争里"API 亲和"比不过"生态引力"。这是 Rust 生态的一个真实伤疤。

monoio 的定位:极致延迟。字节跳动出品,专门为"每核一个线程 + io_uring"架构而生。适用场景:高频交易、搜索引擎内部 RPC、大规模网关。代价是生态单薄、不跨平台(只 Linux + 较新内核)。

glommio 的定位:同 monoio,但更偏数据库场景。Datadog 主推,ScyllaDB 用它。和 monoio 在架构上几乎一样,差异在 API 风格和社区归属——你选哪个取决于你和哪个社区更贴近。

embassy 的定位:嵌入式 / no_std。跑在 MCU 上。绝对的特殊环境专用运行时

pollster 的定位:最简 executor(约 30 行)。不支持 I/O、不支持定时器。用途:给测试代码或纯计算 Future 一个 block_on 实现,不想引入 Tokio 那么重的依赖。

为什么 Tokio 赢了通用场景?

  1. 历史先发:Tokio 是第一个有完整工业栈的 Rust 运行时,早期的 Rust 后端项目(hyper、reqwest、tonic、axum)全部绑定 Tokio,形成了生态锁定
  2. 性能足够好:对于 99% 的业务场景,Tokio 的多线程 work-stealing 调度器已经够快;thread-per-core 的理论优势只在极端场景(延迟敏感、尾延迟敏感)才显现
  3. 可观测性工具链完整tokio-consoletracing 整合、runtime metrics,线上排错所需的全套工具齐全
  4. 稳定性承诺:Tokio 1.x 保证无 breaking change,这对生产项目至关重要

什么时候你应该用 Tokio 之外的运行时?

对本书读者而言,Tokio 是最值得深入学习的对象:学懂 Tokio,其他运行时你都能看懂,反之则不一定成立。

1.3½ Tokio 生态全景:站在巨人肩膀上的一整座大厦

Tokio 本身只是运行时,但真正让你用起来爽的是 Tokio 带动的整个生态。2026 年的今天,一个中高端 Rust 后端服务的典型依赖长这样:

层级 代表库 依赖 Tokio 的什么
HTTP 服务器 axum / actix-web / warp TcpListener、Task、异步 I/O
HTTP 客户端 reqwest / hyper TcpStream、timer、并发控制
gRPC tonic Hyper + HTTP/2
WebSocket tokio-tungstenite TcpStream
数据库 sqlx / sea-orm 连接池、异步 I/O
Redis redis-rs(async) / fred TcpStream、RESP 协议
消息队列 lapin(RabbitMQ)/ rdkafka(Kafka) TcpStream、async channel
对象存储 aws-sdk-rust / object_store Hyper + 信号量限流
日志 / 追踪 tracing / tracing-subscriber Task-local、async span
序列化 serde / rmp-serde 和运行时无关
RPC 框架 tarpc / volo(字节跳动) Tokio 全栈

这座"大厦"有 20+ 主要库,它们全部、毫无例外地绑定 Tokio。为什么?

第一个原因:Hyper hyper 是 Rust 的参考级 HTTP 实现,2014 年由 Sean McArthur 发起。它在 0.10 版本(2018)迁移到 Tokio 之后成为 Tokio 生态的最重要锚点——因为几乎所有 HTTP 相关库都依赖 Hyper。Axum、reqwest、tonic、warp——它们都在 Hyper 之上。Hyper 绑 Tokio,于是上游全都绑 Tokio。

第二个原因:兼容成本 一个库如果想同时支持 Tokio 和 smol,需要:

库作者发现"只支持 Tokio 就够用",因为 99% 的用户用 Tokio。这是典型的生态引力效应——一旦某个选项占了大头,新项目因为"一定要能跟已有依赖共存"也选它,比例进一步扩大。

第三个原因:性能和稳定性 Tokio 多年工业级打磨,生产环境稳定性无可争议。选 Tokio 是最小风险选项——招人好招、问题好 Google、生态丰富、性能有上下文。

Tokio 生态大厦的影响:好的和坏的

好的:你做一个新后端项目,tokio = "1" + axum + sqlx + reqwest 三下五除二搭一个完整服务栈。这体验和 Go / Node.js 打平,不再有 2018 年"Rust 写服务太累"的印象。

坏的:如果你想用 non-Tokio 运行时(比如嵌入式 + smol、超高性能场景 + monoio),生态阻力很大——你可能要自己写或移植大量库。Discord 的后端服务团队曾经一度考虑从 Tokio 迁到 monoio,最后放弃了——迁移成本远超性能收益

你的选择:除非你有非常具体的理由(嵌入式、io_uring 极致延迟),否则拥抱 Tokio 生态。这本书教的就是这个生态的心脏。

一个简单例子:5 行代码搭一个生产级 HTTP 服务

// Axum + Tokio,2026 年 Rust 后端的"hello world"
use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(|| async { "Hello, World!" }));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

5 行代码,可以扛几万 QPS 的稳定 HTTP 服务。这就是 Tokio 生态的成熟度——而 3 年前同样的功能代码量是这个的 3 倍、需要手动写一堆 Hyper 配置。


1.4 Tokio 的顶层架构地图

本节是整本书的"骨架"——下面这张图会贯穿后续 20 章,每一章都在图上的某一块深入钻井。建议你在真正读下去之前、花五分钟把这张图在脑子里过一遍:Runtime 是容器Scheduler 管 Task 调度Drivers 和 OS 对接Waker 是贯穿所有子系统的神经。这四块加一根神经就是整个 Tokio。理解了这个骨架,后面任何一章的深入细节都能找到它在全局中的位置。

现在我们来画 Tokio 的顶层架构图。这张图会贯穿本书 20 章——每一章都在这张图的某一块上深入。

graph TB
    subgraph 用户代码层
        UC["async fn / .await<br/>tokio::spawn<br/>TcpStream::connect"]
    end

    subgraph Runtime["Runtime(顶层容器)"]
        BLD["Builder:配置入口"]
        HND["Handle:外部句柄"]
    end

    subgraph Scheduler["Scheduler(调度器)"]
        MT["multi_thread<br/>work-stealing"]
        CT["current_thread<br/>单线程"]
        LQ["Worker 本地队列"]
        GQ["全局注入队列"]
        LIFO["LIFO slot"]
    end

    subgraph Drivers["Drivers(驱动层)"]
        IOD["I/O Driver<br/>(基于 mio)"]
        TMD["Time Driver<br/>分层定时器轮"]
        SGD["Signal Driver<br/>Unix 信号"]
    end

    subgraph Task["Task 与 Future"]
        TK["Task 结构体<br/>(header + state + future)"]
        WK["Waker<br/>vtable-based"]
        JH["JoinHandle"]
    end

    subgraph Primitives["同步与通信原语"]
        MX["Mutex / RwLock<br/>/ Semaphore"]
        CH["mpsc / broadcast<br/>/ watch / oneshot"]
        NT["Notify"]
    end

    subgraph Observability["可观测性"]
        MT2["Runtime metrics"]
        TR["tracing 集成"]
        TC["tokio-console"]
    end

    UC --> BLD
    UC --> HND
    BLD --> MT
    BLD --> CT
    MT --> LQ
    MT --> GQ
    MT --> LIFO
    MT --> TK
    CT --> TK
    TK --> WK
    TK --> JH
    WK --> MT
    WK --> CT
    MT --> IOD
    MT --> TMD
    CT --> IOD
    CT --> TMD
    IOD -->|epoll_wait 唤醒| WK
    TMD -->|timer 到期| WK
    UC --> MX
    UC --> CH
    MX --> NT
    Runtime --> Observability
    Scheduler --> Observability

这张图里有五个核心子系统一条贯穿的神经

五大子系统

1. Runtime(运行时容器) 顶层入口。tokio::runtime::Runtime 结构体持有 Scheduler + 各 Drivers 的所有权,Handle 是外部拿到 Runtime 引用的句柄。#[tokio::main] 宏展开后本质上就是 Runtime::new().block_on(async { ... })

第 4 章详细拆解 Runtime 的构建与生命周期。

2. Scheduler(调度器) 把"要执行的 Task"和"可用的线程资源"撮合起来。Tokio 有两种调度器:

第 5 章拆解 multi_thread,第 7 章拆解 current_thread。

3. Drivers(驱动层) 运行时的"感官"。Tokio 有三类 Driver:

Driver 的共同模式:被 epoll_wait / timer_fire 等系统调用"唤醒" → 找到对应的 Waker → 调 wake() → Task 被重新放回 Scheduler 队列

第 8-10 章拆解 I/O Driver,第 11 章拆解 Time Driver。

4. Task 与 Future Task 是调度的单位,不等同于 Future。一个 Task 内部拥有一个顶层 Future(通常是 async fn 展开的状态机),Task 的结构体包含:

JoinHandle 是用户侧持有的 Task 引用,可以用来 .await 等待 Task 完成、或 .abort() 取消。

第 6 章拆解 Task 结构。

5. 同步与通信原语 不是基于 Future 就能跑得好的。一个 async 任务等锁、等 channel 消息、等 notify,都需要不阻塞当前 worker 线程的实现。Tokio 的 tokio::sync::Mutex 不是 std::sync::Mutex 的 async wrapper,而是完全不同的实现:它用 Semaphore 的公平排队机制,永远不阻塞 worker,只让 Task 挂起。

第 12-13 章拆解这些原语。

一条贯穿的神经:Waker

Waker 是这整张图的神经。它的接口非常简单(就是一个 fn wake(self)),但它把 Task(被调度的对象)、Scheduler(调度器)、Driver(事件源)三者串起来了:

一个 Waker 是这三方之间的电话线RawWaker + RawWakerVTable 是这条电话线的底层 ABI,它让 Waker 的实现可以是任何东西——不同运行时(Tokio vs smol vs embassy)可以用完全不同的 Task 表达,但都通过同一个 Waker 接口和 Future trait 对话。

第 3 章会把 Waker 拆到 vtable 字节级别。理解了 Waker,你就理解了运行时 ↔ Future 的本质契约。

1.5 一次 .await 在这张图上的完整走位

这一节是整章最值得反复阅读的部分——因为它把前面讲的所有抽象概念(Runtime、Scheduler、Drivers、Waker、Task)串成一条可追踪的完整链路。一次 .await 从用户代码出发、经过 Future::poll 返回 Pending、Driver 向 OS 注册、事件触发、Waker 唤醒 Task、Scheduler 重新调度、Future 再次 poll 返回 Ready——七个环节每一个都有精确的代码位置和数据流向。把这条链路在脑子里走通一次、你就真正拥有了"Tokio 是怎么工作的"的完整心智模型。

把前面的架构落到一个具体例子上:

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut stream = tokio::net::TcpStream::connect("127.0.0.1:8080").await?;
    //                                                               ^^^^^ 这一刻发生了什么?
    let mut buf = vec![0u8; 1024];
    let n = stream.read(&mut buf).await?;
    //                           ^^^^^ 以及这一刻?
    println!("read {} bytes", n);
    Ok(())
}

#[tokio::main] 展开后:

fn main() -> std::io::Result<()> {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?;
    rt.block_on(async {
        let mut stream = tokio::net::TcpStream::connect("127.0.0.1:8080").await?;
        let mut buf = vec![0u8; 1024];
        let n = stream.read(&mut buf).await?;
        println!("read {} bytes", n);
        Ok::<_, std::io::Error>(())
    })
}

接下来按时间顺序展开每一步:

Step 1:Builder::build() 创建 Runtime。构建过程中:

Step 2:rt.block_on(fut)当前线程(主线程)上运行一个 Future,直到它完成:

Step 3:Task 被首次 poll。执行到 TcpStream::connect(...).await

Step 4:三次握手完成。远端回 SYN-ACK,本地内核完成 TCP 三次握手,fd 变为可写:

Step 5:Task 被再次 poll。这次 connect() 内部发现 fd 已经可写了:

Step 6:block_on 循环捕获最终结果。当最外层 Task 返回 Poll::Ready(())block_on 退出,Runtime drop,worker 线程被 join,进程退出。

这一整个流程里,有三点需要记住

  1. Future 本身是被动的——它只会在 poll 到 Pending 时注册 Waker,然后"死在那里",直到被 Waker 唤醒
  2. Driver 是主动的——它等 OS 事件,然后主动调 Waker 唤醒 Task
  3. Scheduler 是撮合者——它决定哪个 worker 跑哪个 Task、什么时候 park/unpark 线程

本书后续 20 章,就是把这一流程中的每一步钻透

这条路径上每一步的"小时间常数"

把 Step 1-6 对应到现代 CPU 的时间尺度,你会有更深的敬畏:

总结:一个"发起 I/O → 等数据 → 继续处理"的 .await 周期,纯 Tokio 开销约 300-500 纳秒。剩下的时间全在内核(epoll、TCP/IP 栈)和你的代码上。Tokio 的运行时开销在整个异步 I/O 链路里占比 < 5%——这就是为什么它能支撑百万 QPS 级别的服务。

这也是为什么 "Tokio 太慢"几乎从来不是真的——当你觉得 Tokio 慢,往往是你的业务代码里某处漏了 spawn_blocking,或者某个 .await 点的 Future 内部有阻塞调用。第 19 章(性能陷阱)会教你怎么辨认这些。

1.6 本书章节与架构地图的对应

把第 1.4 节那张架构图和后续章节对应起来:

子系统 涉及章节
Future / Waker 基础 第 2-3 章(Future 与 poll 模型 + Waker 机制)
Runtime 容器 第 4 章(Runtime 架构总览)
multi_thread Scheduler 第 5 章(多线程 Scheduler 与工作窃取)
current_thread Scheduler 第 7 章(current_thread runtime 与 LocalSet)
Task 结构 第 6 章(Task:轻量级任务的生命周期)
I/O Driver 第 8 章(Reactor 架构)+ 第 9 章(Mio)+ 第 10 章(TcpStream / UdpSocket)
Time Driver 第 11 章(Time Driver 与分层定时器轮)
同步原语 第 12 章(Mutex / RwLock / Semaphore)
channels 第 13 章(mpsc / broadcast / watch / oneshot)
高级原语 第 14 章(select!)+ 第 15 章(JoinHandle / JoinSet)
阻塞任务 第 16 章(spawn_blocking 与 block_in_place)
可观测性 第 17 章(metrics 与 tracing)
工程实践 第 18-20 章(多 runtime、性能调优、设计模式)

读到任何一章感到迷失,请回到本章的架构图和 Step 1-6 的流程。每一章都是在这张图的某一块上深入,从图上找到自己的位置,就不会迷失。

1.6½ 本书读者的几种典型画像

本书不是 Tokio 入门教程(那是 Tokio 官方 tutorial 的工作),而是源码级深度剖析。为了让你读起来不走弯路,我把典型读者画像和对应的推荐阅读路径列出来:

画像一:用过 Tokio 2-3 年,想理解内部机制(本书主要目标读者)

你写过 Axum / Tonic 服务,遇到过几次生产 bug,想知道"Tokio 底下到底怎么工作"。 推荐路径:完整读第 1-10 章(核心机制),然后按需跳第 11-20 章。第 19 章(性能调优与陷阱)你会特别受益。

画像二:正要从 Go / Node.js 迁过来的资深后端

你有扎实的并发 / I/O 背景,但 Rust 是新的。 推荐路径:先读《Rust 编译器与运行时揭秘》第 9-10 章打基础,再读本书。第 1 章和第 5 章会让你快速完成 "Go GMP 到 Tokio" 的心智模型迁移。

画像三:想自己做一个运行时 / 贡献 Tokio 上游

你已经读过部分 Tokio 源码,想系统地理解架构决策。 推荐路径:第 4-6 章(Runtime + Scheduler + Task)是你的核心。第 20 章(设计模式与架构决策)是你的最终目的地。

画像四:做性能敏感服务(交易、实时通信),想调优到极致

你关心 p99 延迟、LIFO slot 行为、work-stealing 开销。 推荐路径:第 5 章 + 第 11 章(Time Driver)+ 第 16 章(blocking)+ 第 19 章(性能调优)。monoio / glommio 的对比在第 1 章有。

画像五:Rust 资深但首次深入异步

你写过 CLI 工具、系统 crate,但一直避开了 async。 推荐路径:先完整读《Rust 编译器与运行时揭秘》第 9-10 章、再读本书第 2-3 章把 Future / Waker 心智建立起来。后面的章节就顺了。

不管哪种画像,我推荐一件事打开 GitHub 上 tokio-rs/tokio 的仓库,本书每引用一段代码,你同步在 GitHub 上找到对应文件看一眼。这个同步阅读习惯对深度理解比任何讲解都管用。源码是唯一不会撒谎的老师


1.6.6 实测:tokio 1.51.1 全家桶 ~103,088 行的"顶层架构地图"

§1.4 给出 Tokio 顶层架构图(Runtime + Scheduler + Drivers + Task)——把它们对应到 ~/.cargo/registry/src/.../tokio-1.51.1/src/ 实测——

全 crate 103,088 行(不含 test、按主要子目录拆分)——

子目录 角色
src/runtime/ 28342 本目录最大——含 §1.4 提到的 Scheduler / Task 调度器 / I/O Driver / Blocking pool 全部基础设施
src/sync/ 21346 mpsc / broadcast / oneshot / watch / Mutex / RwLock / Notify / Semaphore——21K 行专门给"协程间同步原语"
src/net/ 15408 TCP/UDP/Unix Socket 异步包装
src/io/ 13070 AsyncRead/AsyncWrite trait + AsyncBufRead + Util
src/fs/ 4772 文件 I/O(基于 spawn_blocking)
src/task/ 4411 spawn / JoinHandle / yield_now / LocalSet 公共 API
src/process/ 3470 子进程异步 I/O
src/macros/ 2994 select! / pin! / try_join!
src/time/ 2184 sleep / interval / timeout 公共 API
src/signal/ 2001 信号处理
其余(loom/util/runtime/...) ~5090

src/runtime/ 内部进一步拆分——

子目录 文件 角色
runtime/scheduler/ 29 5275 §1.4 主角——current_thread + multi_thread 双调度器
runtime/task/ 15 4753 Task struct(Cell/Header/Core/Trailer)+ Harness + State 管理
runtime/io/ 8 1701 ch08 §8.10⅘ 已测的 I/O Driver 1370 + driver/uring 309
runtime/time_alt/ 13 1551 新版 time wheel(替代旧版)
runtime/blocking/ 5 817 spawn_blocking 线程池

两条值得记住的物理事实——

  1. src/runtime/ 28342 行 = tokio 27%——是 §1.4 顶层架构图的全部底层基础——其中 scheduler/ 5275 + task/ 4753 + io/ 1701 + time_alt/ 1551 + blocking/ 817 = 14097 行是 §1.4 "Runtime + Scheduler + Drivers + Task" 四件套的核心;剩余 14245 行是 builder / context / coop / metrics / handle / park 等支撑代码——印证 §1.4 标题"顶层架构地图"——四件套占 runtime 50%、剩 50% 是 "让四件套配合工作" 的胶水
  2. src/sync/ 21346 行 = tokio 21%——协程间同步原语比 I/O Driver(1701 in runtime/io + 13070 in src/io = 14771)还重 44%——印证 §1.4 提到的"Tokio 不止是 I/O 运行时、也是 async 并发原语库"——每一个 channel/Mutex 都需要把 Future 协议、Waker、抢占、cancellation safety 编织在一起、所以代码量大

对照本书 ch04 §4.11.1 实测的 borrowck 34476 行——tokio 整 crate 103,088 = rustc 借用检查的 3 倍——印证 §1.1 "Rust 把运行时踢出了语言" 的工程含义:运行时由生态承担、生态自己长出与编译器同等量级的工程量

串联 ch08 §8.10⅘ 实测的 I/O Driver 1370 + 本节 runtime 28342 + sync 21346 = ~51K 行——是 Tokio 异步基础设施的核心;用户写 async 代码看不见的复杂度都在这 51K 行里。

1.7 本章小结

这里给一个诚实的劝诫:不要把"Tokio 是主流"等同于"Tokio 适合你所有场景"。做技术选型时,用下面这个清单对照一次:

绝大多数读者的答案是"通用后端服务"。这本书给你的就是这条路的完整地图。

带走三件事:

  1. Rust 把运行时留给了生态——代价是显式依赖,收益是嵌入式 / WASM / 多运行时并存的灵活性。Tokio 是这个空位上最成熟的通用运行时。
  2. Tokio 的架构 = Runtime + Scheduler + Drivers + TaskWaker 是贯穿的神经。后续 20 章都在这张图上深入。
  3. 一次 .await 的完整走位 = Future → Driver 注册 → OS 事件 → Waker 唤醒 → Scheduler 重调度 → Future 再 poll。纯 Tokio 开销约 300-500 纳秒。

延伸阅读