前面讨论的定时方案都是以固定频率调用心搏函数tick,并在其中一次检测到期的定时器,然后执行到期定时器上的回调函数。设计定时器的另一中思路是,将所有定器超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数tick执行,超时时间最小的定时器必然到期。我们就可以从剩余定时器中选出超时时间最小的一个,并将这个时间设为下一次心搏间隔。如此反复,就实现了较为精确的定时。
最小堆适合这种解决方案,下面直接给出最小堆方案的代码,并附有测试用例。不过测试用例我仍然只用了alarm来做测试。
#ifndef MIN_HEAP_H
#define MIN_HEAP_H
#include <iostream>
#include <netinet/in.h>
#include <time.h>
#include <assert.h>
#include <string.h>
const int BUFFER_SIZE = 1024;
class heap_timer;
//绑定socket和定时器
struct client_data {
sockaddr_in addr_;
int sockfd_;
char buf_[BUFFER_SIZE];
heap_timer* timer_;
};
//定时器类
class heap_timer {
public:
heap_timer(int delay) {
printf("birth time %d, delay: %d\n", time(NULL), delay);
expire_ = time(NULL) + delay; //注意,和之前的升序链表以及时间轮不同,这次我们在timer中初始化生效时间
}
public:
void (*timeout_callback_)(client_data*); //定时器的回调函数
public:
time_t expire_; //定时器生效的绝对时间
client_data* user_data_; //用户数据
};
class time_heap {
public:
time_heap(int cap) throw (std::exception) //构造函数之一,初始化一个大小为cap的空堆
: array_(new heap_timer*[cap]), capacity_(cap), cur_size_(0) {
memset(array_, 0, sizeof(array_)); //初始化指针
}
time_heap(heap_timer** init_array, int size, int capacity) throw (std::exception) //构造函数之二,使用已有数组来初始堆
: array_(new heap_timer*[capacity]), cur_size_(size), capacity_(capacity) {
assert(capacity >= size);
memset(array_, 0, sizeof(array_));
if(cur_size_ != 0){
for(int i=0; i<cur_size_; ++i)
array_[i] = init_array[i]; //初始化堆数组
for(int i=((cur_size_-1)>>1); i>=0; --i)
sift_down(i); //多数组中的(cur_size_-1)/2 ~ 0 个元素执行下滤操作
}
}
~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) {
assert(timer != NULL);
if(cur_size_ >= capacity_) //如果当前堆数组容量不够,扩容
resize();
//新插入一个元素,当前堆大小加1,hole是新插入元素的位置
int hole = cur_size_++; //hole = cur_size_ - 1
int parent = 0;
//对从新插入位置到根节点路径上所有节点进行上滤操作
for(; hole > 0; hole = parent){
parent = (hole - 1) >>