⏱️ 深入理解定时器中的【时间轮算法】

👉 时间轮(Timing Wheel) 是:
-
• 定时器 -
• 网络框架(Netty) -
• 游戏服务器 -
• Unity 战斗/BUFF/技能/CD -
• 事件调度系统
里的核心算法之一,属于**“看似基础,实则是对抽象极好的应用”**的内容。
⏱️ 深入理解定时器中的【时间轮算法】
—— 为什么它是 O(1) 的定时器神器?
一、为什么我们需要时间轮?
先从一个真实问题说起。
🎮 Unity 中的常见定时需求
-
• 技能 CD(5s、30s、120s) -
• BUFF 持续时间 -
• DOT / HOT -
• 延迟事件(3 秒后播放动画) -
• 网络心跳 / 超时检测 -
• AI 行为延迟执行
最直观写法:
List<Timer> timers;
foreach (var t in timers)
{
if (Time.time >= t.TriggerTime)
t.Execute();
}
❌ 问题是什么?
-
• 每帧遍历所有定时器 -
• 时间复杂度:O(n) -
• n 大了直接炸(服务器尤甚)
二、传统定时器方案对比(为什么不行)
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
👉 有没有 O(1) 插入 + O(1) 触发?
答案是:时间轮
三、时间轮的核心思想(一句话版)
用“时间槽”代替“时间点”,
把未来的任务提前放进对应的槽里,
时间走到哪,就只处理那个槽。
四、时间轮的直觉模型(钟表模型)

-
• 一个环形数组 -
• 每个格子叫 Slot(槽) -
• 每个槽存多个定时任务 -
• 指针每 Tick 前进一步
五、一个任务如何被调度?
假设:
-
• 时间轮刻度 = 1 秒 -
• 槽数量 = 8 -
• 当前指针在 Slot 2 -
• 新任务:5 秒后执行
计算位置:
slotIndex = (currentIndex + delay) % slotCount
= (2 + 5) % 8
= 7
👉 把任务放进 Slot 7
六、时间轮工作流程(图解)

七、时间复杂度为什么是 O(1)?
插入任务:
-
• 直接计算槽位 -
• 加入 List -
• O(1)
Tick 执行:
-
• 只处理当前槽 -
• O(1)(均摊)
总结:
时间轮用空间换时间,把“排序问题”变成“索引问题”
八、单层时间轮的缺陷
⚠️ 问题来了:
-
• 最大延迟 = slotCount * tickDuration -
• 如果要支持: -
• 1 小时 -
• 24 小时 -
• 7 天
❌ 单层时间轮不够
九、多层时间轮(Hierarchical Timing Wheel)
类比现实钟表:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
多层结构图

-
• 当前层走完一圈 -
• 推动上层 -
• 任务逐级下沉
十、简易版时间轮实现(核心示例)
定时任务结构
public class TimerTask
{
public int RemainingRounds;
public Action Callback;
}
时间轮核心结构
public class TimeWheel
{
private readonly List<TimerTask>[] slots;
private int currentIndex;
public TimeWheel(int slotCount)
{
slots = new List<TimerTask>[slotCount];
for (int i = 0; i < slotCount; i++)
slots[i] = new List<TimerTask>();
}
public void AddTask(int delay, Action callback)
{
int index = (currentIndex + delay) % slots.Length;
int rounds = delay / slots.Length;
slots[index].Add(new TimerTask
{
RemainingRounds = rounds,
Callback = callback
});
}
public void Tick()
{
var list = slots[currentIndex];
for (int i = list.Count - 1; i >= 0; i--)
{
var task = list[i];
if (task.RemainingRounds <= 0)
{
task.Callback();
list.RemoveAt(i);
}
else
{
task.RemainingRounds--;
}
}
currentIndex = (currentIndex + 1) % slots.Length;
}
}
使用示例
TimeWheel wheel = new TimeWheel(60);
void Start()
{
wheel.AddTask(5, () => Debug.Log("5 秒后执行"));
}
void Update()
{
if (Time.frameCount % 60 == 0)
wheel.Tick();
}
十一、时间轮 vs Unity Coroutine
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
👉 服务器 / 大规模定时任务:时间轮完胜
十二、典型应用场景(你可以写进简历)
-
• MMO 技能冷却系统 -
• BUFF 生命周期管理 -
• 网络超时 / 心跳检测 -
• 延迟消息队列 -
• AI 行为调度 -
• ECS 世界 Tick 系统
十三、常见坑点总结
❌ Tick 精度过小 → 槽爆炸
❌ Tick 精度过大 → 任务延迟
❌ 忘记多层轮 → 最大延迟不够
❌ List 未反向遍历 → Remove 崩
❌ 回调中 AddTask → 注意线程安全
十四、终极总结一句话
时间轮不是“更快的排序”,
而是“直接取消排序需求”的时间结构设计。
🎯 面试题
1️⃣ 为什么时间轮插入是 O(1)?
因为通过时间取模直接定位槽位,不需要排序。
2️⃣ 时间轮和小顶堆定时器的核心差异?
堆依赖排序,时间轮依赖索引。
3️⃣ 单层时间轮最大延迟怎么算?
slotCount × tickDuration
4️⃣ 为什么服务器更偏好时间轮?
大量定时任务下,堆的 log n 成本不可控。
5️⃣ Unity 中哪些系统适合时间轮?
技能 CD、BUFF、延迟事件、网络超时。
点赞鼓励下,(づ ̄3 ̄)づ╭❤~
作者:世纪末的魔术师
出处:https://www.cnblogs.com/Firepad-magic/
Unity最受欢迎插件推荐:点击查看
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

浙公网安备 33010602011771号