• 高性能定时器实现


    高性能定时器实现

    1.简介

    对于一个复杂的软件系统,定时器的对任务的管理和调度至关重要,定时器的管理已成为一个复杂系统的重要基础设施。定时器的应用场景非常广泛。在游戏开发中,定时器可以被用于实现游戏中的倒计时、技能冷却等功能,为游戏体验增添乐趣。在物联网领域,定时器可以用于实现设备间的通信、数据上报等功能,为物联网系统的运行提供支持。在数据库中,定时器可以用于实现过期键和延时队列等功能。例如,在 Redis 数据库中,定时器可以用于实现缓存过期,保证缓存数据的时效性。

    2.实现方式

    要想实现高效的定时器,就需要对定时任务进行管理,定时调用(tick)接口获取超时的任务并执行。所以定时器的实现需要满足一下条件:

    • 能够方便地获取过期任务
    • 能够方面增加和删除

    2.1有序链表

    需要排序并且增删方便,很容易想到的一个数据结构就是有序链表,可以将定时任务按照过期时间从小到大排序放入链表。每次tick只需要判断队头task是否到期即可。

    时间复杂度:

    • 插入:O(n)
    • 删除: O(n)
    • 获取到期任务: O(1)

    其中可将要删除的task id单独缓存,在task过期时惰性删除,采用此种方法可以将删除的时间复杂度减少到O(1)

    2.2小顶堆

    有序链表的插入和删除的成本比较高,进一步想到的数据结构就是小顶堆。将定时任务放在小顶堆中管理,每次tick从堆顶获取到的就是最近要过期的task。
    时间复杂度:

    • 插入:O(log2n)
    • 删除: O(log2n)
    • 获取到期任务: O(1)

    陈硕的muduo库,JDKPriorityQueue等定时器时间就是基于小顶堆。

    2.3时间轮

    时间轮(TimingWheel)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表。

    2.3.1单级时间轮

    假设我们的定时任务有最大过期时间限制,比如最多支持(2^8-1=255)ms后过期。那么我们可以

    1.开辟一个TimeWheelSize(2^8)大小的数组TimeWheel,表示一个时间轮,这个时间轮的最小单位为tickms,此处为1ms

    1. 一个表盘指针currentTime,指向当前所处的时间,每次tick向前推进tickms

    流程推演

    1.初始节点currentTime为0。假设此时来了个100ms后过期的task,则将其插入到TimeWheel[100%TimeWheelSize]

    2.currentTime不断推进,直到推进到100,发现TimeWheel[100]中有任务,则将其执行。

    3.若此时又来了个100ms后过期的task,则将其插入到TimeWheel[(currentTime+100)%TimeWheelSize],即TimeWheel[200]中

    4.currentTime不断推进,直到推进到200,发现TimeWheel[200]中有任务,则将其执行。

    5.若此时又来了个100ms后过期的task,则将其插入到TimeWheel[(currentTime+100)%TimeWheelSize],即TimeWheel[44]中

    6.currentTime继续推进,推进到了TimeWheelSize-1,则重置为0. 所以叫做时间轮。然后从0继续推进推进到44时,执行TimeWheel[44]中的任务。

    2.3.2 多级时间轮

    一个时间轮的大小总是有限的,例如上述提到的时间轮最大只支持(2^8-1=255)ms后过期的定时任务。那么如果需要插入256ms后过期的定时任务该怎么办呢?想到钟表上秒针转一圈,分针动一下,分针转一圈后时针动一下,我们定时器也可以采用这种思想使用多级时间轮。

    例如如果需要支持最多(2^32-1)ms(四十多天了,一般足够用了)的延迟任务。则我们可以分为4级时间轮TimeWheel,每个时间轮是一个(2^8-1)大小的数组。其中TimeWheel[0]相当于上述的单级时间轮。

    第N级表盘表盘大小最小单位表盘指针TimeWheelSize
    0TimeWheel[0](2^8-1)1mscurrentTime[0]TimeWheelSize[0]= (2^8)ms
    1TimeWheel[1](2^8-1)(2^8)mscurrentTime[1]TimeWheelSize[1]=(2^16)ms
    2TimeWheel[2](2^8-1)(2^16)mscurrentTime[2]TimeWheelSize[2]=(2^24)ms
    3TimeWheel[3](2^8-1)(2^24)mscurrentTime[3]TimeWheelSize[3]=(2^32)ms

    流程推演

    1. 初始阶段currentTime[0],currentTime[1]都为0。

    2. 此时

      • 插入200ms后到期的任务A、将其插入到TimeWheel[0][200]中。
      • 插入300ms后到期的任务B,发现TimeWheelSize[1]>300>TimeWheelSize[0],则将其插入TimeWheel[1][300%TimeWheelSize[1]] = 则将其插入TimeWheel[1][1]中。
      • 插入400ms后到期的任务C,同理也插入到TimeWheel[1][1]中
    3. currentTime[0]持续推进,直到currentTime[0]=200时执行任务A

    4. currentTime[0]持续推进,直到转了一圈后currentTime[0]=0,此时currentTime[1]+1=1,发现TimeWheel[1][1]中有两个任务B和C。此时B还有44ms过期,C还有144ms过期。则将其从TimeWheel[1][1]中删除并分别插入TimeWheel[0][44],TimeWheel[0][144]中。完成级联。

    多级时间轮级联的流程与上述类似。

    3.总结

    目前用得比较多得时间轮实现是基于小顶堆和基于时间轮实现。

    定时器实现优点缺点适用场景举例
    时间轮时间复杂度为O(1),添加、删除和执行定时任务的时间效率高。支持高并发的定时任务处理。可以使用多级时间轮来处理更大的定时任务范围。精度受到时间轮刻度的限制。定时器链表的长度可能会增长,需要进行定期清理。定时任务较多,且定时间隔较短。需要高并发处理定时任务的场景。操作系统的定时器机制。网络游戏中的定时器处理。
    小顶堆精度高,可以实现毫秒级别的定时任务。可以动态调整定时器的优先级,支持定时器的重新调度。定时器的长度不会随时间增长而增加。时间复杂度为O(logn),添加、删除和执行定时任务的时间效率较低。不支持高并发的定时任务处理。定时任务较少,且定时间隔较长。对定时器精度要求较高的场景。定时器框架Redis中的定时器处理。

    实际实现还有一些优化点。比如采用事件机制,当定时器里有任务到期时才tick,避免无任务到期时频繁调用tick空转。

  • 相关阅读:
    Java笔记——控制台模拟“双色球”福利彩票游戏
    【笔记】html图片映射usemap(vue环境下、map、area、coords)
    Vue---keep-alive组件的使用,缓存组件
    M10C车载SD卡录像机产品外观结构图
    【编程题】【Scratch三级】2021.06 躲球游戏
    跳槽,从这一个坑,跳进另外一个坑
    【仿牛客网笔记】Spring Boot实践,开发社区登录模块-会话管理
    亚马逊云科技数据分析为这伴科技赋能,实现“零”中断目标
    变量、存储过程与函数
    玩转ansys——微机械车轮的实体建模与网格化
  • 原文地址:https://blog.csdn.net/luchengtao11/article/details/133867000