一. 定时器实现
我们知道定时器的实现方式有很多种,最为常见的就数时间堆和时间轮了. 这里介绍时间轮实现的定时器。
时间轮总体来说,对于频繁的插入和删除来说,时间轮的效率较为高些.
二、时间轮算法
1. 单级时间轮算法
如上图就是一个简单的时间轮。类比于时钟的秒针表盘,秒针表盘共 60 秒的刻度,以恒定速度每秒 1 刻度进行读秒。时间轮也是一样,该时间轮共有 N 个 tick 刻度,以恒定速度每次走 1 个 tick 单位时间。每个 tick 对应的是一个双向循环链表,该链表元素为计时器。
处理定时器的过程 :如图,指针指向 1,说明 tick 1 所在的链表中的计时器全部到期,遍历该链表,执行到期的计时器的回调函数。再过 1 tick 时间,表盘指针指向 2,tick 2 所在的链表中的计时器全部到期,然后遍历链表处理到期的计时器。
2. 多级时间轮算法
1) 介绍
还以秒针表盘为例,秒针表盘 60s 需要 60 个刻度,如果是 1h 呢?难道需要 3600 个刻度吗?不是这样的,时钟有 3 级表盘,秒针、分针、时针,它们的粒度分别是 1s、1min、1h。
同样的,表示一个 32bits 以毫秒为单位的时间,粒度为 1ms,如果采用 1 级时间轮算法,则需要 2^32 个 tick,这对于内存空间的消耗非常大。当然,可以降低定时器精度,使每个 tick 表示的时间长一点,但这样的代价将是定时器的精度大打折扣。
linux 内核中的多级时间轮算法采用 5 级时间轮,每级时间轮的粒度分别为:1ms、256ms、25664ms、2566464ms、256646464ms。它们每级时间轮 tick 刻度数量分别为 256(低 8bits)、64(次 6bits)、64(次 6bits)、64(次 6bits)、64(高 6bits)。
2) 处理过程
- 对于一个 32bits 以毫秒为单位的时间 t。tick 取 t 的低 8bits。如果 tick 不为 0,执行步骤 6;如果 tick 为 0,执行步骤 2
- tick 取 t 的次 6 bits,如果 tick 不为 0,取出 2 级时间轮该 tick 的双向循环链表,然后遍历该链表的定时器,根据到期时间来判断将定时器插入到 1 级时间轮还是 2 级时间轮,执行步骤 6。如果 tick 为 0,执行步骤3
- tick 取 t 的次 6 bits,如果 tick 不为 0,取出 3 级时间轮该 tick 的双向循环链表,然后遍历该链表的定时器,根据到期时间来判断将定时器插入到 1 级、2 级、3 级时间轮,执行步骤 6。如果 tick 为 0,执行步骤4
- tick 取 t 的次 6 bits,如果 tick 不为 0,取出 4 级时间轮该 tick 的双向循环链表,然后遍历该链表的定时器,根据到期时间来判断将定时器插入到 1 级、2 级、3 级、4 级时间轮,执行步骤 6。如果 tick 为 0,执行步骤5
- tick 取 t 的高 6 bits,如果 tick 不为 0,取出 5 级时间轮该 tick 的双向循环链表,遍历,根据到期时间来判断将定时器插入到 1 级、2 级、3 级、4 级、5 级时间轮,执行步骤 6
- 遍历 1 级时间轮中该 tick 的双向循环链表,执行这些到期定时器的回调函数
3)特点
- 可添加大量计时器
- 支持从系统启动到 2^32ms 的时间
- 时间精度为毫秒
- 可添加 One-Shot Timer(一次性的计时器),也可添加 Repeating Timer(带有首次触发时间和再次触发时间间隔的计时器)
- 采用多级时间轮算法,节省内存
- 定时器的增删执行时间复杂度均为 O(1)
- 时间轮的运行需要 1 个线程