1. 时间轮解决什么问题
时间轮用于高效管理大量定时任务,例如:
- 请求超时
- 心跳
- 延迟重试
- tokio::time::sleep
- tokio::time::interval
它类似钟表表盘:
- 表盘被划分成多个时间槽。
- 定时任务根据 deadline 放入对应槽位。
- 时间向前推进时,处理已经到期的槽位。
Tokio 使用分层时间轮,类似毫秒、秒、分、时多级表盘:
- 临近任务放在低层,保证较高精度。
- 长期任务放在高层。
- deadline 接近后,任务逐级下沉。
这种结构插入、删除和到期处理通常接近 O(1),适合管理大量定时器。
2. 时间轮与操作系统的分工
时间轮是 Tokio 在用户态实现的能力;操作系统提供:
- 单调时钟
- 线程调度
- 阻塞与唤醒
- I/O 等待机制
可以概括为:
操作系统:负责让线程休眠和醒来
Tokio:负责管理定时器,并决定唤醒哪些异步任务
Tokio 不会为每个 Sleep 创建一个系统线程或系统定时器。
3. Future、Waker 和时间轮
定时 Future 首次被 poll 时:
- 将 deadline 和当前任务的 Waker 注册到时间轮。
- 返回 Poll::Pending。
- 定时器到期后,timer driver 调用 Waker。
- 任务进入 executor 的运行队列。
- 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 被提前唤醒后会:
- 处理 I/O 事件。
- 唤醒相关 Future。
- 检查期间是否已有定时器到期。
- 执行运行队列中的任务。
- 重新查询时间轮中最近的 deadline。
- 计算新的 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。
