bigywater.tech
Rust · 99

Tokio 任务调度

tokio 怎么把任务分给工作线程?一个任务会固定在一个线程上吗?一个不 await 的计算会拖垮什么?这篇用 4 个小程序观察 tokio 调度的真实行为,再看 spawn_blocking 怎么避开这些问题。

4 个线程
1000 个任务分布的工作线程数
150 ms
current_thread 里 ticker 本该在 20 ms 出现,实际出现的时间
12/16
运行过程中换过工作线程的任务数

任务在工作线程间的分布

实测 · 本机执行输出
    线程 ThreadId(2)  完成了 292 个任务
    线程 ThreadId(3)  完成了 291 个任务
    线程 ThreadId(4)  完成了 173 个任务
    线程 ThreadId(5)  完成了 244 个任务
  共 4 个工作线程参与

手写执行器见Async Runtime 做了什么。

任务会迁移

实测 · 本机执行输出
  16 个任务里有 12 个在运行过程中换过工作线程(同一个任务的多次 poll 不一定在同一线程)

单线程上的饿死

实测 · 本机执行输出
  [  0 ms] 开始 150 ms 的纯 CPU 计算(没有 await)
  [150 ms] 计算结束
  [150 ms] ticker 第 1 次(期望每 20 ms 一次)
  [190 ms] ticker 第 2 次(期望每 20 ms 一次)
  [220 ms] ticker 第 3 次(期望每 20 ms 一次)
  [250 ms] ticker 第 4 次(期望每 20 ms 一次)
  [270 ms] ticker 第 5 次(期望每 20 ms 一次)

更多见阻塞代码混入异步任务。

多线程运行时的表现

实测 · 本机执行输出
  [  0 ms] 开始 150 ms 的纯 CPU 计算(没有 await)
  [ 20 ms] ticker 第 1 次
  [ 50 ms] ticker 第 2 次
  [ 70 ms] ticker 第 3 次
  [100 ms] ticker 第 4 次
  [120 ms] ticker 第 5 次
  [150 ms] 计算结束

spawn_blocking

实测 · 本机执行输出
  [ 20 ms] ticker 第 1 次
  [ 50 ms] ticker 第 2 次
  [ 70 ms] ticker 第 3 次
  [100 ms] ticker 第 4 次
  [120 ms] ticker 第 5 次
  [150 ms] 阻塞任务在线程 ThreadId(2) 上完成(不是运行时线程 ThreadId(1)),ticker 全程正常

分步演示

tokio 调度
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。tokio 1.x(rt、rt-multi-thread、time、macros),release 构建
    • 没有验证:tokio 的 LIFO 槽与全局队列的细节、任务窃取的具体策略(只观察到迁移)、coop 预算对只做 I/O 的任务的影响、任务数远多于线程数时的延迟分布、所有工作线程都被占满时的饥饿
    京ICP备2026067502号-1京公网安备11010502063487号