跳转到正文
zeno's blog
返回

Tokio:定时器、时间轮与精度

专题: tokio

Table of contents

Open Table of contents

数据结构:分层时间轮

tokio 的定时器用的是分层时间轮(Hierarchical Timing Wheel),不是 min-heap。

源码在 tokio/src/runtime/time/,设计参考了经典论文 Hashed and Hierarchical Timing Wheels

时间轮(概念上):
  64 个槽,每个槽 1ms,转一圈 = 64ms

  当前指针

  [0][1][2][3][4][5]...[63]

            └─ 这个槽里挂着所有 2ms 后到期的定时器

  每 1ms 指针前进一格,把当前槽里所有到期的定时器 "触发"
  超过 64ms 的定时器存在溢出列表,到时候再重新分配到槽里

为什么不用 min-heap

min-heap(Go 的定时器用这种):
  插入/删除: O(log n)
  取最近的到期项: O(1)
  n 很大时 log n 有开销

时间轮(tokio 的选择):
  插入/删除: O(1)
  tick 检查: O(1) 均摊
  代价: 精度受槽粒度限制,固定内存开销

tokio 的场景是大量短生命周期定时器——每个 sleep、每个 timeout、每个 interval、每个连接的超时检测都是一个定时器。可能同时存在几万个。O(1) 插入比 O(log n) 更重要。

1ms 精度的真实含义

1ms 是时间轮的槽粒度,不是执行精度。

定时器到期时,tokio 不会打断正在执行的 task。它只是把到期的 task 标记为”可运行”,放进调度队列。要等当前 task 让出 CPU 后,调度器才会去 poll 到期的 task。

实际发生的事情:

0ms   时间轮 tick
1ms   时间轮 tick
2ms   时间轮 tick → 发现定时器 A 到期 → task A 加入就绪队列
      调度器开始 poll task A(执行你的逻辑)
3ms   定时器 B 到期了
      但没人检查时间轮 —— CPU 正在跑 task A
4ms   task A 执行完,让出 CPU
      调度器回到事件循环 → 检查时间轮 → 发现定时器 B 早就到期了
      → task B 加入就绪队列 → poll task B
      task B 实际执行时间: 4ms(晚了 1ms)

这是协作式调度的本质限制:没有抢占,不会中断正在运行的 task。

定时器的实际执行时间 = 到期时间 + 等待其他 task 让出 CPU 的时间

精度取决于:
  1. 时间轮槽粒度 (1ms) — tokio 能检测到的最小时间单位
  2. 其他 task 占用 CPU 多久才让出 — 这个不可控

对游戏服务端的影响

服务端掉帧

Game tick 设为 50ms (20Hz):

正常情况:
  tick 1: 0ms  → 逻辑 30ms → 20ms 空闲
  tick 2: 50ms → 逻辑 30ms → 20ms 空闲
  tick 3: 100ms → ...
  每帧都准时 ✓

掉帧情况:
  tick 1: 0ms  → 逻辑 60ms → 超了 10ms!
  tick 2: 60ms → 应该 50ms 触发的,晚了 10ms
  tick 3: 110ms → 连锁延迟
  客户端感觉卡顿 ✗

解决方向

方向 1: 确保每个 tick 逻辑在时间预算内完成
  监控 tick 耗时,报警超过预算的 tick
  histogram!("game_tick_duration_seconds").record(elapsed);

方向 2: 重活不要在 tick 里做
  tick 中只做轻量的状态更新和快照广播
  数据库写入、日志刷盘等 spawn 到独立 task
  tokio::spawn(async move { db.save(snapshot).await; });

方向 3: 跳帧补偿
  如果 tick 晚了,算出跳过了几帧,追赶模拟
  let missed_ticks = elapsed / TICK_DURATION;
  for _ in 0..missed_ticks {
      world.step(TICK_DURATION);
  }

方向 4: 固定时间步长 + 不追赶
  记录上次 tick 时间,每次只推进固定 dt
  即使晚了也只推进一帧,接受"服务端慢放"
  适合对物理一致性要求高的游戏

tokio 定时器 vs 操作系统定时器

                  tokio 定时器              OS 定时器 (timerfd / 信号)
─────────────────────────────────────────────────────────────────
调度方式          协作式(等 task 让出)     抢占式(中断当前执行)
精度             1ms 槽 + task 调度延迟     取决于内核配置(通常 1ms~4ms)
适合             网络 I/O 超时、sleep      硬实时、音视频同步
游戏服务端        够用(20~60Hz tick)       非必要,tokio 足够

对于 20Hz60Hz 的游戏 tick(16ms50ms),tokio 的定时器精度完全够用。关键不是定时器精不精确,而是你的 tick 逻辑跑不跑得完。


分享这篇文章:

上一篇
微服务(三):多实例共享数据库为什么不需要额外同步
下一篇
微服务(二):完整微服务集群的分层系统