高性能定时器实现

高性能定时器实现

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
0 TimeWheel[0] (2^8-1) 1ms currentTime[0] TimeWheelSize[0]= (2^8)ms
1 TimeWheel[1] (2^8-1) (2^8)ms currentTime[1] TimeWheelSize[1]=(2^16)ms
2 TimeWheel[2] (2^8-1) (2^16)ms currentTime[2] TimeWheelSize[2]=(2^24)ms
3 TimeWheel[3] (2^8-1) (2^24)ms currentTime[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空转。

相关推荐
JWASX1 个月前
定时/延时任务-Timer用法
java·定时器·timer
baowxz1 个月前
【MCU】GD32H7定时器使用外部时钟源
定时器·gd32h7·使用外部源定时
草上爬2 个月前
C++笔试题之实现一个定时器
c++·c++11·定时器·timer
阑梦清川2 个月前
JavaEE初阶---多线程(五)---定时器/线程池介绍
java·java-ee·线程池·定时器
景天科技苑2 个月前
【Golang】关于Go语言中的定时器原理与实战应用
后端·golang·定时器·timer·go定时器·go定时任务·ticker
GGBondlctrl2 个月前
【JavaEE初阶】多线程案列之定时器的使用和内部原码模拟
java·开发语言·定时器·timer的使用·定时器代码模拟
三玖诶3 个月前
Qt 中 `QTimer`定时器的使用方法详解
开发语言·qt·定时器·qtimer
林多3 个月前
【Qnx】使用ClockCycles完成计时功能
超时·定时器·时钟周期·qnx·clockcycles
中草药z4 个月前
【JavaEE】线程池和定时器
java·开发语言·java-ee·线程池·多线程·定时器
栗克5 个月前
C# 6.定时器 timer
c#·倒计时·定时器·winform·timer