定时器方案
定时器概述
对于服务端来说,驱动服务端逻辑的事件主要有两个,一个是网络事件,另一个是时间事件。
在不同框架中,这两种事件有不同的实现⽅式:
第⼀种,⽹络事件和时间事件在⼀个线程当中配合使⽤;例如nginx、redis;
第⼆种,⽹络事件和时间事件在不同线程当中处理;例如skynet。
例如:
第一种:
//在一个线程中配合使用
while (!quit) {
int now = get_now_time();// 单位:ms
int timeout = get_nearest_timer() - now;
if (timeout < 0) timeout = 0;
int nevent = epoll_wait(epfd, ev, nev, timeout);
for (int i=0; i<nevent; i++) {
//... ⽹络事件处理
}
update_timer(); // 时间事件处理
}
第二种:
// 第⼆种 在其他线程添加定时任务
void* thread_timer(void * thread_param) {
init_timer();
while (!quit) {
update_timer(); // 更新检测定时器,并把定时事件发送到消息队列中
sleep(t); // 这⾥的 t 要⼩于 时间精度
}
clear_timer();
return NULL;
}
pthread_create(&pid, NULL, thread_timer, &thread_param);
定时器的应用
定时器有很多的应用场景,如:
- 心跳检测
- 技能冷却
- 武器冷却
- 倒计时
- 其他需要使用超时机制的功能
定时器的两个问题
1.定时器如何和别的组件进行交互?
2.定时器误差时间大怎么处理?为什么会误差大?通过定时信号的方式解决。ngix通过在????
定时器的设计
定时器设计的两个关键部分别是对外的接口和组织定时任务的数据结构的选择。
1.接口
一般来说,定时器需要六个接口,它们分别是:
// 初始化定时器
void init_timer();
// 添加定时器
Node* add_timer(int expire, callback cb);
// 删除定时器
bool del_timer(Node* node);
// 找到最近要发⽣的定时任务
Node* find_nearest_timer();
// 更新检测定时器
void update_timer();
// 清除定时器
void clear_timer();
2. 数据结构
为定时器选择的数据结构需要有这些特征:
- 有序的结构,且增加删除操作不影响该结构有序;
- 能快速查找最⼩节点;
为了满足这两个特征,我们选择了红黑树、最小堆和时间轮作为定时器的数据结构。其中红黑树和最小堆一般使用在和事务处理同一个线程中的定时器,时间轮则用在不和事务处理同一个线程中的定时器。
其中红黑树和最小堆不做详述,这里重点讲述下时间轮
时间轮
单层级的时间轮
单层级时间轮由一个顺序表构成,每个存储单元代表一个时间精度。被计时的任务则以链表的形式挂在对应的存储单元上。
单层级时间轮的设计需要重点关注两个部分:
- 时间轮的大小 过大会有空推进的情况 分布式定时器必须要解决的问题 最小堆+单层时间轮。
- 时间精度:每隔多少秒移动一个事件单位
如果用数组作为存储时间事件的数据结构,那么,数组的长度控制在多少比较合适呢?
时间轮的大小小于需要计时的时间,会造成总是检测到没有超时的任务。所以时间轮的大小要大于计时的时间。如果时间轮太大,为什么也会造成空推进的情况.
时间轮大小的计算方法:假设要计时的时间是10s,时间精度是1s,2^n > 10 且2 ^n的最小整数。选择2的指数的原因是CPU在之后对其取余时计算的快。
时间精度:按照我们的需求确定。如果需要计时的时间为10s,就以一秒为精度。
空推进:检测到没有超时的任务。
解决空推进的方法:最小堆+单层级时间轮
多层级时间轮
为什么要多层级时间轮?
如果我们的定时任务的跨度特别大,比如有5s中的,也有2小时的,那么在用单层级的时间轮,就会造成大量空推进的情况。为了解决这个问题,可以使用两个方案:
- 最小堆+单层级时间轮。
- 多层级时间轮。
这里就开始介绍多层级时间轮。
参照时钟表盘的运转规律,可以将定时任务根据触发的紧急程度,分布到不同层级的时间轮中;
假设时间精度为 10ms ;在第 1 层级每 10ms 移动⼀格;每移动⼀格执⾏该格⼦当中所有的定时
任务;
当第 1 层指针从 255 格开始移动,此时层级 2 移动⼀格;层级 2 移动⼀格的⾏为定义为,将该格当中的定时任务重新映射到层级 1 当中;
同理,层级 2 当中从 63 格开始移动,层级 3 格⼦中的定时任务重新映射到层级 2 ; 以此类推层级 4 往层级 3 映射,层级 5 往层级 4 映射;
各级重新映射的方法是定时任务的过期时间对上⼀层级的⻓度取余分布在上⼀层级不同格⼦当中;
其中层级5也是有可能有数据的,当延时任务的触发时间大于时间轮的最大值但超过部分小于四层级最大值时,会被放到层级5的0号格子中。
三种定时器方式的对比:
在多线程环境中使用时间轮时,时间轮的增加和检测节点时o(1),红黑树和最小堆时o(logn),时间轮效率更高。所以在多线程情况下,一般使用时间轮定时。