Rust 笔记 11:tokio 时间轮

1. 时间轮解决什么问题

时间轮用于高效管理大量定时任务,例如:

  • 请求超时
  • 心跳
  • 延迟重试
  • tokio::time::sleep
  • tokio::time::interval

它类似钟表表盘:

  • 表盘被划分成多个时间槽。
  • 定时任务根据 deadline 放入对应槽位。
  • 时间向前推进时,处理已经到期的槽位。

Tokio 使用分层时间轮,类似毫秒、秒、分、时多级表盘:

  • 临近任务放在低层,保证较高精度。
  • 长期任务放在高层。
  • deadline 接近后,任务逐级下沉。

这种结构插入、删除和到期处理通常接近 O(1),适合管理大量定时器。

2. 时间轮与操作系统的分工

时间轮是 Tokio 在用户态实现的能力;操作系统提供:

  • 单调时钟
  • 线程调度
  • 阻塞与唤醒
  • I/O 等待机制

可以概括为:

操作系统:负责让线程休眠和醒来
Tokio:负责管理定时器,并决定唤醒哪些异步任务

Tokio 不会为每个 Sleep 创建一个系统线程或系统定时器。

3. Future、Waker 和时间轮

定时 Future 首次被 poll 时:

  1. 将 deadline 和当前任务的 Waker 注册到时间轮。
  2. 返回 Poll::Pending。
  3. 定时器到期后,timer driver 调用 Waker。
  4. 任务进入 executor 的运行队列。
  5. Executor 再次 poll Future,此时返回 Poll::Ready。

流程如下:

poll Sleep/Interval

注册时间轮,返回 Pending

时间轮到期,调用 Waker

任务进入运行队列

Executor 再次 poll

返回 Ready

定时器到期只代表任务具备运行条件,不代表业务代码会在该时刻立即执行。

4. Tokio 如何使用 epoll

Linux 下,Tokio 通常通过 Mio 使用 epoll 等待 I/O。

epoll_wait 本身支持 timeout:

epoll_wait(epoll_fd, events, max_events, timeout_ms);

Tokio 会从时间轮中找到最近的 deadline,并把剩余时间作为 timeout。

例如最近定时器还有 200ms:

epoll_wait(timeout = 200ms)

它可能因为两种原因返回:

  • I/O 提前到达:返回对应 I/O 事件。
  • 200ms 内没有 I/O:超时返回 0。

超时返回后,Tokio推进时间轮并唤醒到期任务。

重要的是:定时器本身通常没有逐个注册到 epoll。epoll 只负责等待到最近 deadline。

5. I/O 提前到达后的处理

假设:

最近定时器还有 200ms
→ epoll_wait(200ms)
→ 50ms 后 I/O 到达

Tokio 被提前唤醒后会:

  1. 处理 I/O 事件。
  2. 唤醒相关 Future。
  3. 检查期间是否已有定时器到期。
  4. 执行运行队列中的任务。
  5. 重新查询时间轮中最近的 deadline。
  6. 计算新的 timeout,再次调用 epoll_wait。

例如处理 I/O 耗时 20ms,那么原定时器约剩 130ms:

epoll_wait(130ms)

因为时间轮保存的是绝对 deadline,所以不会因 I/O 提前唤醒而丢失定时任务。

6. worker 线程的 park

当 Tokio worker 没有可执行任务时,会进入 park:

等待 I/O
或等待最近定时器
或等待其他线程提交新任务

park 时线程被操作系统阻塞,不占用 CPU。

需要区分:

  • Future Pending:某个异步任务暂停。
  • worker park:整个工作线程暂时没有任务可执行。

有事件后,线程先进入系统就绪状态,获得 CPU 后才能继续运行。

7. 为什么 tick 会晚到

时间轮 deadline 到期后,任务仍需等待 executor 再次 poll。以下因素可能造成晚到:

  • worker 正在执行其他任务;
  • 某个 Future 长时间没有让出执行权;
  • 同一 select! 循环的其他分支正在 .await;
  • runtime 或操作系统调度繁忙;
  • 全量拉取等操作阻塞了定时循环。

因此:

计划到期时间 ≠ 实际 poll 时间

8. Interval 的 missed-tick 策略

Tokio Interval 支持三种策略:

  • Burst:默认,快速补执行错过的 tick。
  • Delay:从实际执行时间重新计算下一周期。
  • Skip:跳过错过的 tick,保持原始周期节奏。

注册心跳通常更适合 Skip,避免长时间阻塞后集中补注册。

此前的 panic 是因为使用:

interval_at(start, Duration::MAX)

tick 晚到超过 5ms 后,默认 Burst 计算:

next = 原计划时间 + Duration::MAX

导致 Instant 溢出。根本原因是用超大周期的 interval 模拟一次性任务;正确实现应使用 sleep。

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇