时间轮使用哈希的思想,将定时器散列到不同的链表上
上图所示的时间轮内,(实线)指针指向轮子上的一个槽(slot
)。它以恒定的速度顺时针转动,每转动一步就指向下一个槽(虚线指针指向的槽),每次转动称为一个滴答(tick
)。一个滴答的时间称为时间轮的槽间隔si
(slot interval),它实际上就是心跳时间。该时间轮共有N个槽,因此它运转一周的时间是
N
∗
s
i
N * si
N∗si。每个槽指向一条定时器链表,每条链表上的定时器具有相同的特征:他们的定时时间相差
N
∗
s
i
N * si
N∗si的整数倍。时间轮正是利用这个关系将定时器散列到不同的链表中。假如现在指针指向槽cs
,我们要添加一个定时时间为ti
的定时器,则该定时器将被插入槽ts
(timer slot)对应的链表中:ts = (cs + ti / si) % N
很显然,对时间轮而言,要提高精度,就要使si值足够小;要提高执行效率,则要求N值足够大。
复杂的时间轮可能有多个轮子,每个轮子拥有不同的粒度
下面是一个简单的时间轮:
#ifndef TIME_WHEEL_TIMER
#define TIME_WHEEL_TIMER
#include <time.h>
#include <netinet/in.h>
#include <stdio.h>
#define BUFFER_SIZE 64
class tw_timer;
// 绑定socket和定时器
class client_data {
public:
sockaddr_in address;
int sockfd;
char buf[BUFFER_SIZE];
tw_timer* timer;
};
// 定时器类
class tw_timer {
public:
tw_timer(int rot, int ts) : next(NULL), prev(NULL), rotation(rot), time_slot(ts) {}
int rotation; // 记录定时器在时间轮转多少圈后生效
int time_slot; // 记录定时器属于时间轮上哪个槽(对应的链表)
void (*cb_func) (client_data*); // 定时器回调函数
client_data* user_data; // 客户数据
tw_timer* next; // 指向下一个定时器
tw_timer* prev; // 指向前一个定时器
};
// 时间轮类
class timer_wheel{
public:
timer_wheel() : cur_slot(0) {
for (int i = 0; i < N; i++) {
slots[i] = NULL; // 初始化每个槽的头结点
}
}
~timer_wheel() {
// 遍历每个槽,并销毁其中的定时器
for (int i = 0; i < N; i++) {
tw_timer* tmp = slots[i];
while (tmp) {
slots[i] = tmp->next;
delete tmp;
tmp = slots[i];
}
}
}
// 根据定时值timeout创建一个定时器,并把它插入合适的槽中
tw_timer* add_timer(int timeout) {
if (timeout < 0) return NULL;
int ticks = 0;
// 下面根据带插入定时器的超时值计算它将在时间轮转动多少个滴答后被触发,并将该滴答数存储于变量ticks中。
// 如果带插入定时器的超时值小于时间轮的槽间隔SI,则将ticks向上取整为1,否则就将ticks向下取整为timeout/SI
if (timeout < SI) ticks = 1;
else ticks = timeout / SI;
// 计算待插入的定时器在时间轮转动多少圈后被触发
int rotation = ticks / N;
// 计算待插入的定时器应该被插入哪个槽中
int ts = (cur_slot + (ticks % N)) % N;
// 创建新的定时器,它在时间轮转动rotation圈之后被触发,且位于第ts个槽上
tw_timer* timer = new tw_timer(rotation, ts);
// 如果第ts个槽中尚无任何定时器,则把新建的定时器插入其中,并将该定时器设置为该槽的头结点
if (!slots[ts]) {
printf("add timer, rotation is %d, ts is %d, cur_slot is %d\n",
rotation, ts, cur_slot);
slots[ts] = timer;
}
// 否则,将定时器插入第ts个槽中
else {
timer->next = slots[ts];
slots[ts]->prev = timer;
slots[ts] = timer;
}
return timer;
}
// 删除目标定时器
void del_timer(tw_timer* timer) {
if (!timer) return;
int ts = timer->time_slot;
// slots[ts]是目标定时器所在槽的头结点。如果目标定时器就是该头结点,则需要重置第ts个槽的头结点
if (timer == slots[ts]) {
slots[ts] = slots[ts]->next;
if (slots[ts]) slots[ts]->prev = NULL;
delete timer;
}
else {
timer->prev->next = timer->next;
if (timer->next) timer->next->prev = timer->prev;
delete timer;
}
}
// SI时间到后,调用该函数,时间轮向前滚动一个槽的间隔
void tick(){
// 取得时间轮上当前槽的头结点
tw_timer* tmp = slots[cur_slot];
printf("current slot is %d\n", cur_slot);
while (tmp) {
printf("tick the timer once\n");
// 如果定时器的rotation值大于0, 则他在这一轮不起作用
if (tmp->rotation > 0) {
tmp->rotation--;
tmp = tmp->next;
}
// 否则,说明定时器已经到期,于是执行定时任务,然后删除该定时器
else {
tmp->cb_func(tmp->user_data);
if (tmp == slots[cur_slot]) {
printf("delete header in cur_slot\n");
slots[cur_slot] = tmp->next;
delete tmp;
if (slots[cur_slot]) slots[cur_slot]->prev = NULL;
tmp = slots[cur_slot];
}
else {
tmp->prev->next = tmp->next;
if (tmp->next) tmp->next->prev = tmp->prev;
tw_timer* tmp2 = tmp->next;
delete tmp;
tmp = tmp2;
}
}
}
// 更新时间轮的当前槽,以反映时间轮的转动
cur_slot = ++cur_slot % N;
}
private:
static const int N = 60; // 时间轮上槽的数目
static const int SI = 1; // 每1s时间轮转动一次,即槽间隔为1s
tw_timer* slots[N]; // 时间轮的槽,其中每个元素指向一个定时器链表,链表无序
int cur_slot; // 时间轮当前槽
};
#endif
可以看到,和有序双向链表相比,定时器的数据结构也有少许变化,同时也保留了一些东西,比如
- 将超时时间
timeout
替换成了记录定时器在转多少圈之后生效的rotation
; - 增加了记录定时器属于时间轮上的哪个槽的
time_slot
- 保留了
next
、prev
两个指针,因为定时器之间还是使用链表相连,只不过有多条链表,且这些链表的头结点存储在了时间轮的各个槽中。
在tick
函数中:
- 每次调用tick时都需要将所有的定时器的
rotation
减1 - 检查当前槽是否有到时的定时器,有则调用回调函数,删除此定时器
- 最后更新当前槽,以反映时间轮的转动
同时add_timer
函数主要作用是计算添加定时器的槽,并将其链接到链表尾部。因为每个链表都是无序的,所以直接链接到尾部即可。