这一篇文章系统的梳理主流定时器算法实现的差别以及应用地方。算法
程序里的定时器主要实现的功能是在将来的某个时间点执行相应的逻辑。在定时器模型中,通常有以下几个定义。 数组
interval:间隔时间,即定时器须要在interval时间后执行数据结构
StartTimer:添加一个定时器任务ide
StopTimer:结束一个定时器任务函数
PerTickBookkeeping: 检查定时器系统中,是否有定时器实例已经到期,至关于定义了最小时间粒度。性能
常见的实现方法有以下几种:优化
链表线程
排序链表3d
最小堆指针
时间轮
接下来咱们一块儿看下这些方法的具体实现原理。
2.1 链表实现
链表的实现方法比较粗糙。链表用于存储全部的定时器,每一个定时器都含有interval 和 elapse 两个时间参数,elapse表示当前被tickTimer了多少次。当elapse 和interval相等时,表示定时器到期。
在此方案中,添加定时器就是在链表的末尾新增一个节点,时间复杂度是 O(1)。若是想要删除一个定时器的话,咱们须要遍历链表找到对应的定时器,时间复杂度是O(n)。此方案下,每隔elapse时间,系统调用信号进行超时检查,即PerTickBookkeeping。每次PerTickBookkeeping须要对链表全部定时器进行 elapse++,所以能够看出PerTickBookkeeping的时间复杂度是O(N)。能够看出此方案过于粗暴,因此使用场景极少
2.2 排序双向链表实现
排序双向链表是在链表实现上的优化。优化思路是下降时间复杂度。
首先,每次PerTickBookkeeping须要自增全部定时器的elapse变量,若是咱们将interval变为绝对时间,那么咱们只须要比较当前时间和interval时间是否相等,减小了对每一个定时器的操做。若是不须要对每一个定时器进行操做,咱们将定时器进行排序,那么每次PerTickBookkeeping都只须要判断第一个定时器,时间复杂度为O(1)。相应的,为了维持链表顺序,每次新增定时器须要进行链表排序时间复杂度为 O(N)。每次删除定时器时,因为会持有本身节点的引用,因此不须要查找其在链表中所在的位置,因此时间复杂度为O(1),双向链表的好处。
图1 双向链表实现示意图
2.3 时间轮实现
时间轮示意图以下:
图2 时间轮
时间轮的数据结构是数组 + 链表。 他的时间轮为数组,新增和删除一个任务,时间复杂度都是O(1)。PerTickBookkeeping每次转动一格,时间复杂度也是O(1)。
2.4 最小堆实现
最小堆是堆的一种, (堆是一种二叉树), 指的是堆中任何一个父节点都小于子节点, 子节点顺序不做要求。
二叉排序树(BST)指的是: 左子树节点小于父节点, 右子树节点大于父节点, 对全部节点适用
图3 最小堆
树的基本操做是插入节点和删除节点。对最小堆而言,为了将一个元素X插入最小堆,咱们能够在树的下一个空闲位置建立一个空穴。若是X能够放在空穴中而不被破坏堆的序,则插入完成。不然就执行上滤操做,即交换空穴和它的父节点上的元素。不断执行上述过程,直到X能够被放入空穴,则插入操做完成。所以咱们能够知道最小堆的插入时间复杂度是O(lgN)。最小堆的删除和插入逻辑基本相似,若是不作优化,时间复杂度也是O(lgN),可是实际实现方案上,作了延迟删除操做,时间复杂度为O(1)。
延迟删除即设置定时器的执行回调函数为空,每次最小堆超时,将触发pop_heap,pop会从新调整最小堆,最终删除的定时器将调整到堆顶,可是回调函数不处理。
能够看到PerTickBookkeeping只处理堆顶定时器,时间复杂度O(1)。最小堆可使用数组来进行表示,数组中,当前下标n的左子节点为2N + 1,当前下标n的右子节点小标为2N + 2。
图4 最小堆的数组表示
3.1 时间复杂度对比
图5 不一样实现时间复杂度
从上面的介绍来看,时间轮的时间复杂度最小、性能最好。
3.2 使用场景来看
在任务量小的场景下:最小堆实现,能够根据堆顶设置超时时间,数组存储结构,节省内存消耗,使用最小堆能够获得比较好的效果。而时间轮定时器,因为须要维护一个线程用来拨动指针,且须要开辟一个bucket数组,消耗内存大,使用时间轮会较为浪费资源。在任务量大的场景下:最小堆的插入复杂度是O(lgN), 相比时间轮O(1) 会形成性能降低。更适合使用时间轮实现。在业界,服务治理的心跳检测等功能须要维护大量的连接心跳,所以时间轮是首选。
更多免费技术资料及视频