本文展示两种高性能定时器(理论+代码):时间轮和时间堆
时间轮
基于排序链表的定时器存在一个问题:添加定时器的效率偏低,时间轮则解决了这个问题。
- 如图所示的时间轮内,(实现)指针指向轮子上的一个槽(slot)。它以恒定的速度顺时针转动,每次转动称为一个滴答(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
#define TIME_WHEEL_TIMER
#include <time.h>
#include <stdio.h>
#include <netinet/in.h>
#define BUFFER_SIZE 64
class tw_timer;
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;//表示在转动多少个滴答后触发定时器
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, ratation is %d, ts is %d, cur_slot is %d\n",
rotation, ts, cur_slot);
slots[ts] = timer;
} 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;
//如果刚好是头节点
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
static const int SI = 1;
//时间轮的槽,其中每个元素指向一个定时器链表,链表无序,头插法
tw_timer *slots[N];
int cur_slot;// 时间轮的当前槽
};
#endif
时间堆
- 前边讨论的定时方案都是以固定频率调用心搏函数tick,并在其中依次检测到期的定时器,然后执行到期定时器上的回调函数。设计定时器的另外一种思路是:将所有定时器中超时时间最小的一个定时器的超时值(expire)作为心搏间隔,这样,一旦心搏函数tick被调用,超时时间最小的定时器必然到期。
- 于是我们想到了最小堆(小顶堆)这种数据结构,每次拿第一个结点的超时时间作为心搏间隔,保证每次调用tick函数都会有到期的定时器。
时间堆代码展示
#ifndef MIN_HEAP
#define MIN_HEAP
#include <iostream>
#include <netinet/in.h>
#include <time.h>
using std::exception;
using namespace std;
#define BUFFER_SIZE 64
class heap_timer; //前置声明
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;
}
//添加目标定时器timer
void add_timer(heap_timer* timer) throw (std::exception) {
if( ! timer ) {
return;
}
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;//细节点
}
//将堆数组容量扩大一倍
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;//堆数组当前包含元素的个数
};
#endif
从执行效率来看
- 基于升序链表的定时器
添加定时器的时间复杂度为O(n),删除定时器的时间复杂度为O(1),执行定时任务的时间复杂度为O(1)。 - 时间轮
添加定时器的时间复杂度为O(1),删除定时器的时间复杂度为O(1),执行定时任务的时间复杂度为O(n)。 但实际上执行一个定时器任务的效率要比O(n) 好得多,因为时间轮上所有的定时器散列到了不同的链表上,况且这里我们只用了一个时间轮,当使用多个轮子来实现时间轮时,效率会更高(接近O(1) )。 - 时间堆
添加定时器的时间复杂度为O(lgn),删除定时器的时间复杂度为O(1),执行定时任务的时间复杂度为O(1)。