定时器通常需要包含两个成员,一个超时时间和一个任务回调函数。有时候通常还包括回调函数被执行时需要传入的参数,以及是否重启定时器等信息。
基于双向链表的定时器,在执行效率上来看,添加定时器的时间复杂度为 O ( n ) O(n) O(n),删除定时器的时间复杂度为 O ( 1 ) O(1) O(1),执行定时任务的时间复杂度为 O ( 1 ) O(1) O(1)。
时间堆
添加一个定时器的时间复杂度为 O ( l o g n ) O(logn) O(logn),删除一个定时器的时间复杂度为 O ( 1 ) O(1) O(1),执行一个定时器的时间复杂度为 O ( 1 ) O(1) O(1),时间堆中删除一个定时器的时间复杂度为 O ( 1 ) O(1) O(1)是因为直接把回调函数置空即可。
时间轮
添加一个定时器的时间复杂度是 O ( 1 ) O(1) O(1),删除一个定时器的时间复杂度也是 O ( 1 ) O(1) O(1),执行一个定时器的时间复杂度为 O ( n ) O(n) O(n)。
图11-1所示的时间轮内,指针指向轮子上的一个槽。它以恒定的速度顺时针转动,每转动一步就指向下一个槽(虚线指针指向的槽),每次转动称为一个滴答(tick
)。一个滴答的时间称为时间轮的槽时间si(slot interval)
,它实际上就是心搏时间。该时间轮共有 N N N个槽,因此它运转一周的时间是 N ∗ s i N*si N∗si。每个槽指向一条定时器链表,每条链表上的定时器具有相同的特征:它们的定时时间相差 N ∗ s i N*si N∗si的整数倍。时间轮正是利用这个关系将定时器散列在不同的链表中。假如现在指针指向槽cs
,我们要添加一个定时时间为ti
的定时器,则该定时器将被插入槽ts
对应的链表中:
t s = ( c s + ( t i / s i ) ) % N ts = (cs+(ti/si)) \% N ts=(cs+(ti/si))%N
基于排序链表的定时器使用唯一的一条链表来管理所有定时器,所以插入操作的效率随着定时器数目的增多而降低。而时间轮使用哈希表的思想,将定时器散列到不同的链表上。这样每条链表上的定时器数目都将明显少于原来的排序链表上的定时器数目,插入操作的效率基本不受定时器数目的影响。
很显然,对时间轮而言,要提高定时精度,就要使si
值足够小;要提高执行效率,则要求N足够大。
图11-1描述的是一种简单的时间轮,因为它只有一个轮子。而复杂的时间轮可能有多个轮子,不同的轮子拥有不同的粒度。相邻的两个轮子,精度高的转一圈,精度低的仅往前移动一槽,就像水表一样。
下面实现一个较为简单的时间轮实现代码。
#ifndef TIME_WHEEL_TIMER
#define TIME_WHEEL_TIMER
#include
#include
#include
#define BUFFER_SIZE 64
class tw_timer;
//绑定socket和定时器
struct client_data
{
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){}
public:
//记录定时器在时间轮转多少圈后生效
int rotation;
//记录定时器属于时间轮上哪个槽(对应的链表,下同)
int time_slot;
//定时器回调函数
void (*cb_func)(client_data*);
//客户数据
client_data* user_data;
tw_timer* next; //指向下一个定时器
tw_timer* prev; //指向前一个定时器
};
class time_wheel
{
public:
time_wheel():cur_slot(0)
{
for(int i = 0; i < N; ++i)
slots[i] = NULL; //初始化每个槽的头节点
}
~time_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;
}
//删除目标定时器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;
//每1s时间轮转动1次,即槽间隔为1s
static const int SI = 1;
//时间轮的槽,其中每个元素指向一个定时器链表,链表无序
tw_timer* slots[N];
//时间轮的当前槽
int cur_slot;
};
#endif
对于时间轮而言,添加一个定时器的时间复杂度是 O ( 1 ) O(1) O(1),删除一个定时器的时间复杂度也是 O ( 1 ) O(1) O(1),执行一个定时器的时间复杂度是 O ( n ) O(n) O(n)。实际上执行一个定时器任务的效率要比 O ( n ) O(n) O(n)好得多,因为时间轮将所有的定时器散列到了不同的链表上。时间轮的槽越多,等价于散列表的入口越多,从而每条链表上定时器数量越少。此外,我们的代码仅使用了一个时间轮。当使用多个轮子来实现时间轮时,执行一个定时器任务的时间复杂度将接近 O ( 1 ) O(1) O(1)。
设计定时器的另外一种思路是:将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数tick
被调用,超时时间最小的定时器必然到期,我们就可以在tick
函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。如此反复,就实现了较为精确的定时。
最小堆很适合处理这种定时方案。最小堆是指每个节点的值都小于或等于其子节点的值的完全二叉树。
树的基本操作是插入节点和删除节点。
对最小堆而言,它们都很简单。为了将一个元素X插入最小堆,我们可以在树的下一个空闲位置创建一个空穴。如果X可以放在空穴中而不破坏堆序,则插入完成。否则就执行向上操作,即交换空穴和它的父节点上的元素。不断执行上述操作,直到X可以被放入空穴,则插入操作完成。
比如,我们要往图11-2所示的最小堆中插入值为14的元素,则可以按照图11-3所示的步骤来操作。
最小堆的删除操作指的是删除其根节点上的元素,并且不破坏堆序性质。执行删除操作时,我们需要现在根节点处创建一个空穴。由于堆现在少了一个元素,因此我们可以把堆的最后一个元素X移动到该堆的某个地方。如果X可以被放入空穴,则删除操作完成。否则就执行向下操作,即交换空穴和两个儿子节点中的较小者。不断进行上述过程,直到X可以被放入空穴。则删除操作完成。
比如,我们要对图11-2所示的最小堆执行删除操作,则可以按照图11-4所示的步骤来执行。
由于最小堆是一个完全二叉树,所以可以用数组来组织其中的元素。比如,图11-2所示的最小堆可以用图11-5所示的数组来表示。对于数组中的任意一个位置 i i i上的元素,其左儿子节点在位置 2 i + 1 2i+1 2i+1上,其右儿子节点在位置 2 i + 2 2i+2 2i+2上,其父节点在位置 [ ( i − 1 ) / 2 ] ( i > 0 ) [(i-1)/2](i>0) [(i−1)/2](i>0)上。与链表相比,用数组表示堆不仅节省空间,而且更容易实现堆的插入,删除等操作。
假设我们已经有一个包含 N N N个元素的数据,现在要把它初始化为一个最小堆。最简单的方法是:初始化一个空堆,然后将数组中的每个元素插入该堆中。不过这样做的效率偏低。实际上,我们只需要对数组中的第 [ ( N − 1 ) / 2 ] − 0 [(N-1)/2] - 0 [(N−1)/2]−0个元素执行向下操作,即可确保该数组构成一个最小堆。这是因为对包含 N N N个元素的完全二叉树而言,它具有 [ ( N − 1 ) / 2 ] [(N-1)/2] [(N−1)/2]个非叶子节点,这些非叶子节点正是该完全二叉树的第 0 − [ ( N − 1 ) / 2 ] 0 - [(N-1)/2] 0−[(N−1)/2]个节点。我们只要确保这些非叶子节点都具有堆序性质,整个树就具有堆序性质。
最小堆实现的定时器为时间堆。
#ifndef MIN_HEAP
#define MIN_HEAP
#include
#include
#include
using std::exception;
#define BUFFER_SIZE 64
class heap_timer; //前向声明
//绑定socket和定时器
struct client_data
{
sockaddr_in address;
int sockfd;
char buf[BUFFER_SIZE];
heap_timer* timer;
};
//定时器类
class heap_timer
{
public:
heap_timer(int delay)
{
expire = time(NULL) + delay;
}
public:
time_t expire; //定时器生效的绝对时间
void (*cb_func)(client_data*); //定时器的回调函数
client_data* user_data; //用户数据
};
//时间堆表
class time_heap
{
public:
//构造函数之一,初始化一个大小为cap的空堆
time_heap(int cap) throw(std::exception) : capacity(cap), cur_size(0)
{
array = new heap_timer* [capacity]; //创建堆数组
if(!array)
throw std::exception();
for(int i = 0; i < capacity; ++i)
{
array[i] = NULL;
}
}
//构造函数之二,用已有数组来初始化堆
time_heap(heap_timer** init_array, int size, int capacity) throw(std::exception) : cur_size(size), capacity(capacity)
{
if(capacity < size)
throw std::exception();
array = new heap_timer* [capacity]; //创建堆数组
if(!array)
throw std::exception();
for(int i = 0; i < capacity; ++i)
array[i] = NULL;
if(size != 0)
{
//初始化堆数组
for(int i = 0; i < size; ++i)
{
array[i] = init_array[i];
}
for(int i = (cur_size - 1) / 2; i >= 0; --i)
{
//对数组中的第[(cur_size - 1) / 2]~0个元素执行向下操作
percolate_down(i);
}
}
}
//销毁时间堆
~time_heap()
{
for(int i = 0; i < cur_size; ++i)
delete array[i];
delete []array;
}
public:
//条件目标定时器timer
void add_timer(heap_timer* timer) throw (std::exception)
{
if(!timer)
return;
//如果当前堆数组容量不够,则将其扩大1倍
if(cur_size >= capacity)
{
resize();
}
//新插入了一个元素,当前堆大小加1,hole是新建空穴的位置
int hole = cur_size++;
int parent = 0;
//对从空穴到根节点的路径上的所有节点执行向上操作
for(;hole > 0; hole = parent)
{
parent = (hole - 1) / 2;
if(array[parent]->expire <= timer->expire)
break;
array[hole] = array[parent];
}
array[hole] = timer;
}
//删除目标定时器timer
void del_timer(heap_timer* timer)
{
if(!timer)
return;
/*
* 仅仅将目标定时器的回调函数设置为空,即所谓的延迟销毁,
* 这将节省真正删除该定时器造成的开销,但这样做容易使堆数组膨胀
*/
timer->cb_func = NULL;
}
//获取堆顶部的定时器
heap_timer* top() const
{
if(empty())
return NULL;
return array[0];
}
//删除堆顶部的定时器
void pop_timer()
{
if(empty())
return;
if(array[0])
{
delete array[0];
//将原来的堆顶元素替换为堆数组中最后一个元素
array[0] = array[--cur_size];
percolate_down(0); //对新的堆顶元素执行向下操作
}
}
//心跳函数
void tick()
{
heap_timer* tmp = array[0];
time_t cur = time(NULL); //循环处理堆中到期的定时器
while(!empty())
{
if(!tmp)
break;
//如果堆顶定时器没有到期,则退出循环
if(tmp->expire > cur)
{
break;
}
//否则就执行堆顶定时器中的任务
if(array[0]->cb_func)
{
array[0]->cb_func(array[0]->user_data);
}
//将堆顶元素删除,同时生成新的堆顶定时器array[0]
pop_timer();
tmp = array[0];
}
}
bool empty() const { return cur_size == 0; }
private:
//最小堆的向下操作,它确保堆数组中第hole个节点作为根的子树拥有最小堆性质
void percolate_down(int hole)
{
heap_timer* temp = array[hole];
int child = 0;
for(; ((hole*2+1) <= (cur_size - 1)); hole = child)
{
child = hole * 2 + 1;
if((child < (cur_size - 1)) && (array[child+1]->expire < array[child]->expire))
++child;
if(array[child]->expire < temp->expire)
array[hole] = array[child];
else
break;
}
array[hole] = temp;
}
//将堆数组容量扩大1倍
void resize() throw(std::exception)
{
heap_timer** temp = new heap_timer* [2*capacity];
for(int i = 0; i < 2*capacity; ++i)
{
temp[i] = NULL;
}
if(!temp)
throw std::exception();
capacity = 2 * capacity;
for(int i = 0; i < cur_size; ++i)
temp[i] = array[i];
delete []array;
array = temp;
}
private:
heap_timer** array; //堆数组
int capacity; //堆数组的容量
int cur_size; //堆数组当前包含元素的个数
};
对于时间堆而言,添加一个定时器的时间复杂度 O ( l o g n ) O(logn) O(logn),删除一个定时器的时间复杂度是 O ( 1 ) O(1) O(1),执行一个定时器的时间复杂度是 O ( 1 ) O(1) O(1)。