时间轮(Timing Wheel)算法-高性能定时器策略笔记

原文转自:http://www.tanjp.com/archives/199 (即时修正和更新)

 

什么时候需要定时器?

我们都知道程序是能快速运算出结果,几乎在一瞬间就可以把结果算出来。但是这个前提是所有输入条件都拿到手的情况下,如果有些输入条件 A 你并不知道什么时候能符合,那怎么办?写一个whlie循环一直检查?这样无疑很浪费CPU,显然行不通。

有经验程序员可以已经想到办法,把这些等待输入条件 A, B, C, …等等全部记录起来,在其他相关事件触发时,顺便检查一下这些输入条件是否满足?如果满足就执行某个函数,否则下次再检查一次。这种做法,一般的业务系统都是可实现的。但是会导致代码繁琐而且也不好维护,并且当检查条件多了会导致系统性能大幅下降。这时候,就需要使用定时器来定时检查。

还有一种情况是,假设某个游戏战斗逻辑,一个法术攻击使得某个区域内中毒,持续时间从[t1, t2]。也就是说,在未来确定的时间点会发生某些事情的时候,就要在未来的某个时间点添加定时器事件。

图文代码请参照以上笔记

 

时间轮(Timing Wheel)算法

时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。时间轮的时间格个数是固定的,可用wheelSize来表示,那么整个时间轮的总体时间跨度(interval)可以通过公式 tickMs × wheelSize计算得出。时间轮还有一个表盘指针(currentTime),用来表示时间轮当前所处的时间,currentTime是tickMs的整数倍。currentTime可以将整个时间轮划分为到期部分和未到期部分,currentTime当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的链表的所有任务。

若时间轮的tickMs=1ms,wheelSize=20,那么可以计算得出interval为20ms。初始情况下表盘指针currentTime指向时间格0,此时有一个定时为2ms的任务插入进来会存放到时间格为2的链表中。随着时间的不断推移,指针currentTime不断向前推进,过了2ms之后,当到达时间格2时,就需要将时间格2所对应的链表中的任务做相应的到期操作。此时若又有一个定时为8ms的任务插入进来,则会存放到时间格10中,currentTime再过8ms后会指向时间格10。如果同时有一个定时为19ms的任务插入进来怎么办?如果此时有个定时为350ms的任务该如何处理?当任务的到期时间超过了当前时间轮所表示的时间范围时,就会尝试添加到上层时间轮中。

图文代码请参照以上笔记

参考上图,复用之前的案例,第一层的时间轮tickMs=1ms, wheelSize=20, interval=20ms。第二层的时间轮的tickMs为第一层时间轮的interval,即为20ms。每一层时间轮的wheelSize是固定的,都是20,那么第二层的时间轮的总体时间跨度interval为400ms。以此类推,这个400ms也是第三层的tickMs的大小,第三层的时间轮的总体时间跨度为8000ms。

 

对于之前所说的350ms的定时任务,显然第一层时间轮不能满足条件,所以就升级到第二层时间轮中,最终被插入到第二层时间轮中时间格17所对应的链表中。如果此时又有一个定时为450ms的任务,那么显然第二层时间轮也无法满足条件,所以又升级到第三层时间轮中,最终被插入到第三层时间轮中时间格1的链表中。注意到在到期时间在[400ms,800ms)区间的多个任务(比如446ms、455ms以及473ms的定时任务)都会被放入到第三层时间轮的时间格1中,时间格1对应的链表的超时时间为400ms。随着时间的流逝,当次链表到期之时,原本定时为450ms的任务还剩下50ms的时间,还不能执行这个任务的到期操作。这里就有一个时间轮降级的操作,会将这个剩余时间为50ms的定时任务重新提交到层级时间轮中,此时第一层时间轮的总体时间跨度不够,而第二层足够,所以该任务被放到第二层时间轮到期时间为[40ms,60ms)的时间格中。再经历了40ms之后,此时这个任务又被“察觉”到,不过还剩余10ms,还是不能立即执行到期操作。所以还要再有一次时间轮的降级,此任务被添加到第一层时间轮到期时间为[10ms,11ms)的时间格中,之后再经历10ms后,此任务真正到期,最终执行相应的到期操作。

 

代码实现C++

1、基础节点类型和链表元素的添加和删除。图文代码请参照以上笔记

 

2、时间轮,一个轮子。图文代码请参照以上笔记

 

3、轮组,多个轮子组合起来。图文代码请参照以上笔记

 

4、封装接口,方便易用。图文代码请参照以上笔记

 

应用例子

添加一个Id为10的1000毫秒的定时器,触发5次后结束。还有添加一个Id为20的3000毫秒的定时器,无限触发。

图文代码请参照以上笔记

 

输出结果:

begin

now=1558125768212—–

now=1558125768220, times=8, had=0 —–

now=1558125769225, times=1005, had=1 —–

expired tid=10

—————-

now=1558125770237, times=1012, had=1 —–

expired tid=10

—————-

now=1558125771245, times=1008, had=1 —–

expired tid=20

expired tid=10

—————-

now=1558125772252, times=1007, had=1 —–

expired tid=10

—————-

now=1558125773256, times=1004, had=1 —–

expired tid=10

closed tid=10

—————-

now=1558125774261, times=1005, had=1 —–

expired tid=20

—————-

now=1558125775269, times=1008, had=0 —–

now=1558125776271, times=1002, had=0 —–

now=1558125777283, times=1012, had=1 —–

expired tid=20

—————-

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章