Rust in Everything
基于 tokio 1.54.1

crate 依赖图:workspace 分层与外部依赖

主线位置:这一页不在主线上。它回答「tokio 由哪几个 crate 组成、又依赖了谁」,给后面各章提到 mio、bytes、tokio-util 时一个落脚点。

workspace 里的 10 个成员

根 Cargo.toml 第 3 行列出了 10 个成员,其中 5 个发布到 crates.io,5 个只在仓库内部用:

crate1.54.1 tag 上的版本作用
tokio1.54.1运行时本体,这一卷读的就是它
tokio-macros2.7.2#[tokio::main]、#[tokio::test],以及 select! 内部用的两个辅助过程宏
tokio-util0.7.20codec、CancellationToken、DelayQueue 等工具
tokio-stream0.1.19把 tokio 的类型包成 Stream
tokio-test0.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 的 sync feature(tokio-util 第 38 行、tokio-stream 第 43 行),其余 feature 由它们自己的 feature 一一转发。比如 tokio-util 的 time = ["tokio/time", "slab"](第 29 行):用户不开 tokio-util 的 time,就不会因为它多编译一个定时器。
  • tokio-stream 对 tokio-util 的依赖只在 sync feature 下(第 37 行)。用到它的只有两处:BroadcastStream 和 WatchStream 要用 tokio_util::sync::ReusableBoxFuture(broadcast.rs 第 6 行)。
  • tokio-test 固定打开 test-util(第 20 行),所以依赖了 tokio-test 的测试里,time::pause() 一定可用。
📝NOTE
没有蠢问题

问: 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 }
📝NOTE
冷知识:~ 是 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 里干什么
mionet、signal、processepoll / kqueue 的跨平台封装,IO 驱动的底座,见反应器
socket2netTcpSocket 在 bind 之前设置 socket 选项(socket.rs 第 92 行)
bytesio-util、process给 ReadBuf 实现 bytes::BufMut(read_buf.rs 第 279 行),让 read_buf 能直接写进 BytesMut
signal-hook-registrysignal、process在进程级注册 Unix 信号处理函数(signal/unix.rs 第 286 行)
parking_lot同名 feature,full 包含它替换内部的 Mutex / RwLock / Condvar
libcnet、signal、process 等直接调用系统接口
windows-sysWindows 上的 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 数清单
不开1pin-project-lite
rt-multi-thread1pin-project-lite
rt-multi-thread,macros,net,time8libc mio pin-project-lite proc-macro2 quote socket2 syn unicode-ident
full17上面 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拉进来用途
codeclibc(Unix)LinesCodec 用 libc::memchr 找分隔符(第 50 行)
timeslabDelayQueue 的存储(第 45 行)
rtfutures-util任务相关工具
join-maphashbrownJoinMap 按键索引任务
compatfutures-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。

💬 关联讨论

发起讨论
加载中...