crate 依赖图:workspace 分层与外部依赖
主线位置:这一页不在主线上。它回答「tokio 由哪几个 crate 组成、又依赖了谁」,给后面各章提到
mio、bytes、tokio-util时一个落脚点。
workspace 里的 10 个成员
根 Cargo.toml 第 3 行列出了 10 个成员,其中 5 个发布到 crates.io,5 个只在仓库内部用:
| crate | 1.54.1 tag 上的版本 | 作用 |
|---|---|---|
tokio | 1.54.1 | 运行时本体,这一卷读的就是它 |
tokio-macros | 2.7.2 | #[tokio::main]、#[tokio::test],以及 select! 内部用的两个辅助过程宏 |
tokio-util | 0.7.20 | codec、CancellationToken、DelayQueue 等工具 |
tokio-stream | 0.1.19 | 把 tokio 的类型包成 Stream |
tokio-test | 0.4.6 | 测试辅助:assert_ready!、模拟 IO 的 io::Builder |
benches、examples、stress-test、tests-build、tests-integration | 不发布 | 基准、示例、压力测试、feature 组合测试、跨 crate 集成测试 |
发布 crate 之间的分层
只看 [dependencies](不含 dev),5 个 crate 排成四层:
flowchart TB macros[tokio-macros 过程宏] tokio[tokio 运行时本体] util[tokio-util] stream[tokio-stream] test[tokio-test] tokio -->|feature macros| macros util --> tokio stream --> tokio stream -.->|feature sync| util test --> tokio test --> stream
图里箭头是「依赖」,虚线是可选依赖。几处值得看一眼:
tokio-macros在最底层。它是proc-macro = true的 crate(第 20 行),只依赖proc-macro2、quote、syn,不依赖 tokio。它生成的代码里写的是tokio::runtime::Builder,到了用户的 crate 里才去解析,所以它自己不需要链接 tokio。tokio-util和tokio-stream只要求tokio的syncfeature(tokio-util 第 38 行、tokio-stream 第 43 行),其余 feature 由它们自己的 feature 一一转发。比如 tokio-util 的time = ["tokio/time", "slab"](第 29 行):用户不开 tokio-util 的time,就不会因为它多编译一个定时器。tokio-stream对tokio-util的依赖只在syncfeature 下(第 37 行)。用到它的只有两处:BroadcastStream和WatchStream要用tokio_util::sync::ReusableBoxFuture(broadcast.rs 第 6 行)。tokio-test固定打开test-util(第 20 行),所以依赖了 tokio-test 的测试里,time::pause()一定可用。
问: tokio-macros 生成的代码里写死了 tokio::runtime::Builder。如果我在 Cargo.toml 里给 tokio 改了名(tokio1 = { package = "tokio", ... }),#[tokio1::main] 还能用吗?
答: 不加参数就不能。展开出来的路径是字面上的 tokio,在你的 crate 里找不到,编译报 use of unresolved module or unlinked crate `tokio` 。宏为这种情况留了一个 crate 参数:写成 #[tokio1::main(crate = "tokio1")],parse_knobs 就用这个路径代替 tokio,生成 use tokio1::runtime::Builder;。两种写法都在 tokio 1.54.1 上编译试过。
dev-dependency 里的环
反过来看 dev-dependencies,图里会出现环:tokio 的测试用到 tokio-test 和 tokio-stream,而这两个又依赖 tokio。Cargo 允许这种环,因为 dev-dependency 只参与编译测试,不参与编译库本身:先把 tokio 库编好,再拿它去编 tokio-test,最后编 tokio 的测试。
但版本要对得上。tokio 的 dev-dependency 写的是 tokio-test = "0.4.0",如果从 crates.io 下载,下载到的 tokio-test 会再从 crates.io 拉一个 tokio,测试里就同时存在两个 tokio,类型互不兼容。根 Cargo.toml 用 [patch.crates-io] 把 5 个 crate 全部指回本地路径,整个 workspace 里就只有一个 tokio:
[patch.crates-io]
tokio = { path = "tokio" }
tokio-macros = { path = "tokio-macros" }
tokio-stream = { path = "tokio-stream" }
tokio-test = { path = "tokio-test" }
tokio-util = { path = "tokio-util" }
tokio 本体的外部依赖:几乎全由 feature 决定
tokio/Cargo.toml 的 [dependencies] 只有一个必选依赖,紧接着就是一行注释:
tokio-macros = { version = "~2.7.0", optional = true }
pin-project-lite = "0.2.11"
# Everything else is optional...
bytes = { version = "1.2.1", optional = true }
mio = { version = "1.2.0", optional = true, default-features = false }
parking_lot = { version = "0.12.0", optional = true }
~ 是 tokio-macros 2.0 时加上的~2.7.0 的意思是「不低于 2.7.0,低于 2.8.0」:一个 tokio 版本只接受同一个 minor 系列的 tokio-macros。tokio 1.26.0 里这一行还是普通的 "1.7.0",接受 1.x 里任何不低于 1.7.0 的版本。2023 年 3 月发布 tokio-macros 2.0.0 的 #5580 把它改成 ~2.0.0,随 tokio 1.27.0 发布。同一个 PR 写进更新日志的说明是:从此 tokio-macros 的大版本会升得更勤,这能减少一种困惑,就是同一个版本的 Tokio,因为碰巧用了两个不同版本的 tokio-macros,表现却不一样。
pin-project-lite 是用声明宏实现的 pin 投影,在 39 个源文件里出现 42 次 pin_project!,Sleep(钉住内部的定时器条目)、Timeout(钉住内层 future 和它的 Sleep)都靠它安全地拿到字段的 Pin<&mut _>。它不是过程宏,不会把 syn 拉进来。
其余外部依赖按 feature 拉进来。只画对 macOS / Linux 生效的部分:
flowchart LR macros[feature macros] --> tm[tokio-macros] --> syn[syn / quote / proc-macro2] ioutil[feature io-util] --> bytes[bytes] net[feature net] --> mio[mio] net --> socket2[socket2] net --> libc[libc] signal[feature signal] --> mio signal --> shr[signal-hook-registry] process[feature process] --> mio process --> shr process --> bytes pl[feature parking_lot] --> plc[parking_lot] uring[feature io-uring 需 tokio_unstable] --> iou[io-uring] uring --> slab[slab] dump[feature taskdump 需 tokio_unstable] --> bt[backtrace]
图里最值得注意的是没出现的那几个 feature:rt、rt-multi-thread、sync、time、fs 不引入任何外部依赖(features 第 29 行起)。第 3、4 章读到的任务内存布局、本地队列、任务窃取,第 6 章的时间轮,第 7 章的信号量和通道,全部是 tokio 自己写的,没有借用 crossbeam 之类的库。外部依赖集中在和操作系统打交道的边缘:
| 外部 crate | 由谁拉进来 | 在 tokio 里干什么 |
|---|---|---|
mio | net、signal、process | epoll / kqueue 的跨平台封装,IO 驱动的底座,见反应器 |
socket2 | net | TcpSocket 在 bind 之前设置 socket 选项(socket.rs 第 92 行) |
bytes | io-util、process | 给 ReadBuf 实现 bytes::BufMut(read_buf.rs 第 279 行),让 read_buf 能直接写进 BytesMut |
signal-hook-registry | signal、process | 在进程级注册 Unix 信号处理函数(signal/unix.rs 第 286 行) |
parking_lot | 同名 feature,full 包含它 | 替换内部的 Mutex / RwLock / Condvar |
libc | net、signal、process 等 | 直接调用系统接口 |
windows-sys | Windows 上的 net、signal、process | 同上,Windows 版 |
parking_lot 是唯一一个「不加 API、只换实现」的 feature。切换发生在 loom/std/mod.rs 第 55 行:打开时内部锁用 parking_lot 的,不打开用标准库的。这个切换层和 loom 测试是同一层,见源码地图里的 loom 一节。
tracing 也是可选依赖,但它挂在 cfg(tokio_unstable) 下,而且注释写着 # Not in full(第 110 行):full 不打开它,要单独开 tracing feature 并加 --cfg tokio_unstable。
实测:不同 feature 组合拉进多少个 crate
上面的图是读 Cargo.toml 得来的,可以用 cargo tree 对一遍。新建一个空 crate,tokio 指向 tokio-1.54.1 tag 的本地检出,在 macOS(aarch64-apple-darwin)上跑:
cargo tree -e normal,build --target aarch64-apple-darwin -F tokio/full --prefix none
去重、去掉实验 crate 本身后的结果:
| tokio 的 feature | 外部 crate 数 | 清单 |
|---|---|---|
| 不开 | 1 | pin-project-lite |
rt-multi-thread | 1 | pin-project-lite |
rt-multi-thread,macros,net,time | 8 | libc mio pin-project-lite proc-macro2 quote socket2 syn unicode-ident |
full | 17 | 上面 8 个,加 bytes cfg-if errno lock_api parking_lot parking_lot_core scopeguard signal-hook-registry smallvec |
第二行印证了前面的结论:只要调度器,tokio 不比空 crate 多依赖任何东西。macros 带来的 4 个是过程宏工具链,只在编译期运行,不进最终二进制。full 多出来的 9 个里,parking_lot 一家带了 5 个(parking_lot_core、lock_api、scopeguard、smallvec、cfg-if);errno 是 signal-hook-registry 带进来的。
tokio-util 的外部依赖
tokio-util 也是按 feature 拉依赖,但必选的多一些:bytes、futures-core、futures-sink、pin-project-lite 不管开不开 feature 都在(第 37 行起),因为 codec 的 Framed 要实现 futures 的 Stream 和 Sink。可选的几个各有归属:
| tokio-util feature | 拉进来 | 用途 |
|---|---|---|
codec | libc(Unix) | LinesCodec 用 libc::memchr 找分隔符(第 50 行) |
time | slab | DelayQueue 的存储(第 45 行) |
rt | futures-util | 任务相关工具 |
join-map | hashbrown | JoinMap 按键索引任务 |
compat | futures-io | 和 futures 的 AsyncRead / AsyncWrite 互转 |
小结
- workspace 里发布 5 个 crate:
tokio-macros在最底层,不依赖 tokio;tokio-util、tokio-stream只要求 tokio 的sync,其余 feature 逐个转发;tokio-test在最上层。 - dev-dependency 构成的环靠
[patch.crates-io]指回本地路径,保证 workspace 里只有一个 tokio。 - tokio 本体唯一必选的外部依赖是
pin-project-lite。调度器、任务、定时器、同步原语都不引入外部 crate,外部依赖集中在 IO、信号、进程这些和操作系统打交道的地方。 - 实测在 macOS 上:只开
rt-multi-thread是 1 个外部 crate,full是 17 个,其中 5 个来自parking_lot。