1 定时器应用场景
a. 批量处理定时任务
b. 心跳检测
c. 和网络事件一起使用
2 时间轮
2.1 什么是时间轮
- 时间是一种调度模型, 为高效解决调度任务而产生的
2.2 为什么要用时间轮
在讨论为什么定时器要用时间轮之前, 我们先了解一下几种其他实现方式的调度器
a. 有序队列
- 添加/删除任务: 遍历每一个节点, 找到相应的位置插入, 因此时间复杂度为O(n)
- 处理到期任务: 取出最小定时任务为首节点, 因此时间复杂度为O(1)
b. 红黑树
有序队列的性能瓶颈在于插入任务和删除任务(查找排序), 而树形结构能对其进行优化, 这里可以以红黑树为例子
- 添加/删除/查找任务: 红黑树能将排序的的时间复杂度降到O(log2N)
- 处理到期任务: 红黑树查找到最后过期的任务节点为最左侧节点, 因此时间复杂度为O(log2N)
c. 最小堆
- 添加/查找任务: 时间复杂度为O(log2N)
- 删除任务: 时间复杂度为O(n), 可通过辅助数据结构(map)来加快删除操作
- 处理到期任务: 最小节点为根节点, 时间复杂度为O(1)
d. 跳表
- 添加/删除/查找任务: 时间复杂度为O(log2N)
- 处理到期任务: 最小节点为最左侧节点, 时间复杂度为O(1), 但空间复杂度比较高, 为O(1.5n)
不难看出上面的方法在添加/删除定时任务时都要话比较长的时间, 特别是定时任务越多, 所花费的时间就越多, 因此就需要一种方法能够减少添加/删除任务时所花费的时间, 而这就能借助时间轮来实现了
** 题外话: 具体选择用哪种数据结构来实现定时器还是得根据实际的使用场景, 这里就不对这几种数据结构进行详细分析 **
2.3 时间轮的原理
就像钟表那样:
- 低层级(秒针)
- 高层级(分针)
- 低层级每次时间+1就移动一格(最小精度), 低层级如果移动一圈后, 高层级就移动一格
2.4 时间轮的实现
在实现完整的时间轮(多层级)之前我们需要理解单层级时间轮的实现
2.41 单层级时间轮
单层级时间轮指的是像秒表那样, 只有秒针, 没有分针和时针, 因此只能定时很小范围内的定时任务(0~60s), 但理解它对之后引申到多层级时间轮有很大帮助
1)首先我们来讨论如何用数据结构来抽象秒针的运转
int seconds[60]; // 数组来表示表盘刻度
++tick % 60; // 每秒钟移动一格(++tick); 对tick取余则使秒针能一直落在[0, 59]的区间上
2)那么如何添加定时任务呢?
添加定时任务就是在当前的时间节点加上定时时间, 然后再%60映射到seconds[60]数组相应的位置上即可
拓展一下:
- 也许你会想到, 如果在同一节点下有多个定时任务该怎么办
这里我们就可以修改下
int seconds[60]
, 将int
变为一个链表的结构体即可, 那么我们每次添加新的任务就可以用链表链起来;
即如果改时间节点下有其他任务, 就将新添加的任务加到链表尾部;
typedef struct timer_node {
struct timer_node *next;
} timer_node_t;
typedef struct link_list {
timer_node_t head;
timer_node_t *tail;
} link_list_t;
typedef struct timer {
link_list_t seconds[60];
} s_timer_t;
该图为上面结构体示意图
3)执行任务:
当移到到相应的区间上时, 就将该区间所以的任务节点取出来;
4)取消任务:
- 因为我们添加任务时并没有记录节点添加的位置, 无法找到任务节点是在哪里, 所以我们可以在任务节点的结构体上添加一个取消任务的字段;
在执行任务时, 就会检查这个字段的真假, 如果为真便是取消了的任务;
那么遇到取消了的任务时, 就可以不去执行相应的定时任务即可
typedef struct timer_node {
struct timer_node *next;
uint8_t cancel; // 该字段用来判断任务是否被取消
} timer_node_t;
2.42 多层级时间轮
单层级时间轮只能定时[0, 59]区间的时间, 而超过这个时间的话是无法解决的, 所以在实际环境中并不适用, 这时候我们可以在它的基础上拓展为多层级时间轮
- 多层级时间轮是在单层级时间轮的基础上加多几层, 如图所示:
注: 该图单层级时间轮的定时区间我们现在设为[0, 255]
1)多层级时间轮的原理
a.第一层和上面单层级的一样, 第二层当第一层tick == 256时, 第二层指针+1; 第n层为上一层指针到达数组大小时, 指针+1
b.每次当指针指到相应的数组格子时, 就将该格子的链表取出, 并重新映射到上一层的数组上
c.当第一层指针指到相应的数组格子时, 则该格子下的链表任务到期(即单层级情况)
2)如何确定定时的时间是加在哪个层级上的哪个数组位置?
- 多层级第一层: [0, 256),
(定时时间 - 第一层最大时间)%该层格子数
, 第一层最大时间:第一层数组大小
- 多层级第二层: [256, 256 * 64),
(定时时间 - 前两层最大时间)%该层格子数
, 前两层最大时间:第一层数组大小 + (第一层数组大小 * 第二层数组大小)
- 多层级第n层: 同上以此类推
2.5 代码实现
https://github.com/SPWT/server_learn/tree/main/7_1_timer