上次说到,基于排序链表的定时器存在一个问题:添加定时器的效率偏低。这次我们用时间轮来解决该问题。
如图就是一个时间轮:
在时间轮内,指针指向轮子上的一个槽。它以恒定的速率顺时针转动。没转动一步就指向下一个槽,每次转动称之为一个tick。一个滴答的时间称为时间轮的槽间隔si(slot interval),它实际上就是心搏时间。时间轮共有N个槽,因此它运转一周的时间是N*si。每个槽指向一个定时器链表,每条链表上的定时器具有相同的特征:它们的定时时间相差N*si的整数倍。时间轮正式利用这个关系将定时器散列到不同的链表中。加入现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插入槽ts(timer slot)对应的链表中:
ts = (cs + (ti / si)) % N
基于排序链表的定时器使用唯一的链表来管理所有定时器,所以插入操作的效率随着定时器数目的增多而降低。而时间轮使用哈希表的思想,将定时器散列到不同的链表上。这样每条链表上的定时器数目都将明显少于原来的排序链表上的定时器数目,插入操作的效率基本不受定时器数目的影响。
很显然,对时间轮而言,要提高定时精度,就要使si值足够小;要提高执行效率,则要求N值足够大。
上图描述的是一个简单的时间轮,仅仅一个轮子。而复杂的时间轮可能有多个轮子,不同轮子拥有不同的粒度。
下面是一个简单时间轮的实现代码:
#ifndef TIME_WHEEL_TIMER_H
#define TIME_WHEEL_TIMER_H
#include <time.h>
#include <netinet/in.h>
#include <stdio.h>
#include <assert.h>
const int BUFFER_SIZE = 1024;
class tw_timer;
//绑定socket和定时器
struct client_data {
sockaddr_in addr_;
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)
{}
public:
void (*timeout_callback_)(client_data*); //定时器回调函数
public:
int rotation_; //记录定时器在时间轮转多少圈后生效,因为有的定时值比较大
int time_slot_; //记录定时器对应于时间轮上的哪个槽(对应的链表)
client_data *user_data_; //客户数据
tw_timer* next_; //指向上一个定时器
tw_timer* prev_; //指向下一个定时器
};
class time_wheel {
public:
time_wheel() : cur_slot_(0) {
memset(slots_, 0, sizeof(slots_)); //清零每个槽指针
}
~time_wheel(){
//遍历每个槽,并销毁其中的定时器
for(int i=0; i<DEFAULT_SLOTS_NUM; ++i){
tw_timer* tmp = slots_[i];
while(tmp != NULL){
slots_[i] = tmp->next_;
delete tmp;
tmp = slots_[i];
}
}
}
public:
tw_timer* add_timer(int timeout);
tw_timer* adjust_timer(tw_timer* timer, int timeout);
void del_timer(tw_timer* timer);
void tick();
private:
static const int DEFAULT_SLOTS_NUM = 60;
static const int SI = 1;
tw_timer* slots_[DEFAULT_SLOTS_NUM];
int cur_slot_;
};
//根据定时值timeout创建一个定时器,并把它插入合适的槽中