高效定时器的实现方式

来源:互联网 发布:淘宝总销售额查询 编辑:程序博客网 时间:2024/05/17 04:55

参考:http://www.ibm.com/developerworks/cn/linux/l-cn-timers/


定时器是使用很多的一个组件,如何实现一个高效的定时器?一般来说有四种基本方式:1.链表 2.排序链表 3.最小堆 4.基于时间轮。

首先介绍定时器的基本模型:

StartTimer(Interval, TimerId, ExpiryAction)

注册一个时间间隔为 Interval 后执行 ExpiryAction 的定时器实例,其中,返回 TimerId 以区分在定时器系统中的其他定时器实例。

StopTimer(TimerId)

根据 TimerId 找到注册的定时器实例并执行 Stop 。

PerTickBookkeeping()

在一个 Tick 内,定时器系统需要执行的动作,它最主要的行为,就是检查定时器系统中,是否有定时器实例已经到期。注意,这里的 Tick 实际上已经隐含了一个时间粒度 (granularity) 的概念。

ExpiryProcessing()

在定时器实例到期之后,执行预先注册好的 ExpiryAction 行为。

上面说了基本的定时器模型,但是针对实际的使用情况,又有以下 2 种基本行为的定时器:

Single-Shot Timer

这种定时器,从注册到终止,仅仅只执行一次。

Repeating Timer

这种定时器,在每次终止之后,会自动重新开始。本质上,可以认为 Repeating Timer 是在 Single-Shot Timer 终止之后,再次注册到定时器系统里的 Single-Shot Timer,因此,在支持 Single-Shot Timer 的基础上支持 Repeating Timer 并不算特别的复杂。


基于链表实现的很容易理解,就是直接把每个定时器作为链表的一个节点,要找到到时间的定时器每次就得从头到尾遍历一遍,判断定时器是否到时间,因此执行时间是O(n)

基于排序链表就是在插入的时候按照时间大小排序,因此执行的时候只需要遍历前面几个满足条件的定时器就可以了,因此执行时间是O(1),但是插入时间是O(n)

图 1. 基于排序链表的定时器
基于排序链表的定时器


基于最小堆的就是满足插入时间是O(logn),执行的时候就和排序链表差不多

而最后介绍的时间轮定时器满足插入,执行时间,找到最小的时间复杂度都是O(1)

时间轮 (Timing-Wheel) 算法类似于一以恒定速度旋转的左轮手枪,枪的撞针则撞击枪膛,如果枪膛中有子弹,则会被击发;与之相对应的是:对于 PerTickBookkeeping,其最本质的工作在于以 Tick 为单位增加时钟,如果发现有任何定时器到期,则调用相应的 ExpiryProcessing 。设定一个循环为 N 个 Tick 单元,当前时间是在 S 个循环之后指向元素 i (i>=0 and i<= N - 1),则当前时间 (Current Time)Tc 可以表示为:Tc = S*N + i ;如果此时插入一个时间间隔 (Time Interval) 为 Ti 的定时器,设定它将会放入元素 n(Next) 中,则 n = (Tc + Ti)mod N = (S*N + i + Ti) mod N = (i + Ti) mod N 。如果我们的 N 足够的大,显然 StartTimer,StopTimer,PerTickBookkeeping 时,算法复杂度分别为 O(1),O(1),O(1) 。在 [5] 中,给出了一个简单定时器轮实现的定时。下图 3 是一个简单的时间轮定时器:

图 3. 简单时间轮
简单时间轮

如果需要支持的定时器范围非常的大,上面的实现方式则不能满足这样的需求。因为这样将消耗非常可观的内存,假设需要表示的定时器范围为:0 – 2^3-1ticks,则简单时间轮需要 2^32 个元素空间,这对于内存空间的使用将非常的庞大。也许可以降低定时器的精度,使得每个 Tick 表示的时间更长一些,但这样的代价是定时器的精度将大打折扣。现在的问题是,度量定时器的粒度,只能使用唯一粒度吗?想想日常生活中常遇到的水表,如下图 4:

图 4. 水表
水表

在上面的水表中,为了表示度量范围,分成了不同的单位,比如 1000,100,10 等等,相似的,表示一个 32bits 的范围,也不需要 2^32 个元素的数组。实际上,Linux 的内核把定时器分为 5 组,每组的粒度分别表示为:1 jiffies,256 jiffies,256*64 jiffies,256*64*64 jiffies,256*64*64*64 jiffies,每组中桶的数量分别为:256,64,64,64,64,这样,在 256+64+64+64+64 = 512 个桶中,表示的范围为 2^32 。有了这样的实现,驱动内核定时器的机制也可以通过水表的例子来理解了,就像水表,每个粒度上都有一个指针指向当前时间,时间以固定 tick 递增,而当前时间指针则也依次递增,如果发现当前指针的位置可以确定为一个注册的定时器,就触发其注册的回调函数。 Linux 内核定时器本质上是 Single-Shot Timer,如果想成为 Repeating Timer,可以在注册的回调函数中再次的注册自己。内核定时器如下图 5:

图 5. Linux 时间轮
Linux时间轮



时间复杂度如下:

由上面的分析,可以看到各种定时器实现算法的复杂度:

表 1. 定时器实现算法复杂度
实现方式StartTimerStopTimerPerTickBookkeeping基于链表O(1)O(n)O(n)基于排序链表O(n)O(1)O(1)基于最小堆O(lgn)O(1)O(1)基于时间轮O(1)O(1)O(1)

0 0
原创粉丝点击