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

 

image

 

👉 时间轮(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 大了直接炸(服务器尤甚)

二、传统定时器方案对比(为什么不行)

方案
数据结构
插入
触发
问题
List
数组
O(1)
O(n)
每帧扫描
SortedList
有序数组
O(n)
O(1)
插入慢
小顶堆
PriorityQueue
O(log n)
O(log n)
高频触发压力大
红黑树
Tree
O(log n)
O(log n)
实现复杂

👉 有没有 O(1) 插入 + O(1) 触发?

答案是:时间轮


三、时间轮的核心思想(一句话版)

用“时间槽”代替“时间点”,
把未来的任务提前放进对应的槽里,
时间走到哪,就只处理那个槽。


四、时间轮的直觉模型(钟表模型)

 

image
  • • 一个环形数组
  • • 每个格子叫 Slot(槽)
  • • 每个槽存多个定时任务
  • • 指针每 Tick 前进一步

五、一个任务如何被调度?

假设:

  • • 时间轮刻度 = 1 秒
  • • 槽数量 = 8
  • • 当前指针在 Slot 2
  • • 新任务:5 秒后执行

计算位置:

slotIndex = (currentIndex + delay) % slotCount
          = (2 + 5) % 8
          = 7

👉 把任务放进 Slot 7


六、时间轮工作流程(图解)

image


七、时间复杂度为什么是 O(1)?

插入任务:

  • • 直接计算槽位
  • • 加入 List
  • • O(1)

Tick 执行:

  • • 只处理当前槽
  • • O(1)(均摊)

总结:

时间轮用空间换时间,把“排序问题”变成“索引问题”


八、单层时间轮的缺陷

⚠️ 问题来了:

  • • 最大延迟 = slotCount * tickDuration
  • • 如果要支持:
    • • 1 小时
    • • 24 小时
    • • 7 天

❌ 单层时间轮不够


九、多层时间轮(Hierarchical Timing Wheel)

类比现实钟表:

层级
现实
槽数
Level 0
秒针
60
Level 1
分针
60
Level 2
时针
24

多层结构图

image

 

Level 0 (Seconds)
Level 1 (Minutes)
Level 2 (Hours)
  • • 当前层走完一圈
  • • 推动上层
  • • 任务逐级下沉

十、简易版时间轮实现(核心示例)

定时任务结构

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

维度
Coroutine
时间轮
大量定时
❌ GC 压力
精度
帧级
Tick 级
可控性
服务器适配
性能
极高

👉 服务器 / 大规模定时任务:时间轮完胜


十二、典型应用场景(你可以写进简历)

  • • MMO 技能冷却系统
  • • BUFF 生命周期管理
  • • 网络超时 / 心跳检测
  • • 延迟消息队列
  • • AI 行为调度
  • • ECS 世界 Tick 系统

十三、常见坑点总结

❌ Tick 精度过小 → 槽爆炸
❌ Tick 精度过大 → 任务延迟
❌ 忘记多层轮 → 最大延迟不够
❌ List 未反向遍历 → Remove 崩
❌ 回调中 AddTask → 注意线程安全


十四、终极总结一句话

时间轮不是“更快的排序”,
而是“直接取消排序需求”的时间结构设计。

🎯 面试题

1️⃣ 为什么时间轮插入是 O(1)?

因为通过时间取模直接定位槽位,不需要排序。


2️⃣ 时间轮和小顶堆定时器的核心差异?

堆依赖排序,时间轮依赖索引。


3️⃣ 单层时间轮最大延迟怎么算?

slotCount × tickDuration

4️⃣ 为什么服务器更偏好时间轮?

大量定时任务下,堆的 log n 成本不可控。


5️⃣ Unity 中哪些系统适合时间轮?

技能 CD、BUFF、延迟事件、网络超时。

posted @ 2025-12-25 16:17  世纪末の魔术师  阅读(25)  评论(0)    收藏  举报