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 逻辑跑不跑得完。