(1)時間堆的原理
定時器以到期時間作爲排序值,存放於最小堆這種數據結構中。時間堆不以固定的頻率來查詢是否有定時器到期,而是每次當堆頂的定時器到期後才處理一次定時事件,避免了定期查詢導致的開銷。
(2)難點分析及解決思路
Q:如何確定堆頂定時器是否到期?
A:仍然以定時的方式來提醒進程或線程有定時器到期,但定時時長是變化的,其值始終等於堆頂定時器到期的絕對時刻,與定時時刻之差;
Q:當插入一個新的定時器,或刪除一個定時器後,堆頂定時器可能會發生變化,因此定時時長也就可能變化,如何及時更新定時時長呢?
A:藉助epoll來監聽,插入或刪除定時器這一事件是否發生,一旦發生,則立即修改定時時長;
Q:如何實現定時呢?
A:可以使用alarm函數實現。但其實可以利用epoll_wait函數中的超時參數來進行定時。如果時間堆中無定時器,則超時參數爲-1,促使其阻塞,直到有定時器插入事件發生;否則,超時參數等於定時時長。當定時器插入或刪除事件發生後,epoll_wait函數返回,隨即重新確定定時時長,並修改超時參數,以用於下一次的epoll_wait函數調用。
(3)時間堆框架
(4)源代碼及註釋
#ifndef TIME_HEAP
#define TIME_HEAP
#include <time.h>
#include <vector>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <stdio.h>
#include <pthread.h>
template <typename T> //T是客戶類
class th_timer //定時器類
{
public:
time_t expire; //定時器到期的絕對時刻
T *user_data; //客戶數據,應包含此定時器指針
void (*cb_func)(T*); //回調函數,即當定時器到期時,要執行的操作
int loc; //定時器在時間堆中的位置,從堆中刪除定時器時會用到
public:
th_timer(time_t _expire, T *_user, void (*_cb_func)(T*))
:expire(_expire), user_data(_user), cb_func(_cb_func) { };
};
template <typename T>
class time_heap //時間堆類
{
private:
static const int INIT_SIZE = 50; //時間堆的初始容量
static const int MAX_EVENT_NUMBER = 10; //epoll內核事件表所能容納的事件的最大數量
std::vector<th_timer<T>*> min_heap; //用於表示時間堆的vector數組,從[1]開始使用
int hsize; //堆的實際大小
int epollfd; //指向epoll內核事件表的fd
int pipefd[2]; //通信管道,當插入或刪除定時器事件發生時,用來傳遞信息
private: //私有工具函數
void check_size()
// 檢查時間堆是否已裝滿,若是則擴充容量
{
if(hsize == min_heap.size() - 1)
{
min_heap.resize(2 * min_heap.size());
}
}
void swap(int loc_a, int loc_b)
// 給定時間堆中的兩個位置,交換這兩處的元素,定時器記錄的自身位置也要及時更新
{
th_timer<T> *temp = min_heap[loc_a];
min_heap[loc_a] = min_heap[loc_b];
min_heap[loc_a]->loc = loc_a; //更新定時器自身位置
min_heap[loc_b] = temp;
min_heap[loc_b]->loc = loc_b; //更新定時器自身位置
}
void move_down(int rt)
// 從位置rt開始下移操作。當最小堆中的某個元素突然變大時,可用此函數維護最小堆的性質。
{
int lc = rt * 2, rc = lc + 1, min_loc = rt;
if(lc <= hsize && min_heap[lc]->expire < min_heap[min_loc]->expire)
{
min_loc = lc;
}
if(rc <= hsize && min_heap[rc]->expire < min_heap[min_loc]->expire)
{
min_loc = rc;
}
if(min_loc != rt)
{
swap(min_loc, rt);
move_down(min_loc); //遞歸下移
}
}
void move_up(int rt)
// 從位置rt開始上移操作。當最小堆中的某個元素突然變小時,可用此函數維護最小堆的性質。
{
th_timer<T>* timer = min_heap[rt];
int pos = rt;
for(;rt > 1 && timer->expire < min_heap[pos/2]->expire;pos /= 2)
{
min_heap[pos] = min_heap[pos / 2];
min_heap[pos]->loc = pos;
}
min_heap[pos] = timer;
min_heap[pos]->loc = pos;
}
static void* run(void *arg)
//線程的執行函數,用於監聽插入或刪除定時器事件的發生,並及時修改定時時長,處理定時事件
{
time_heap* th = (time_heap*)arg; //獲取時間堆類對象
//將通信管道[0]端插入epoll內核事件表內
epoll_event event;
event.data.fd = th->pipefd[0];
event.events = EPOLLIN | EPOLLET;
epoll_ctl(th->epollfd, EPOLL_CTL_ADD, th->pipefd[0], &event);
epoll_event events[MAX_EVENT_NUMBER];
bool stop = false;
int timeout = -1; //超時參數,剛啓動時堆中無定時器,因此令epoll_wait阻塞,直到有定時器插入
while(!stop)
{
int number = epoll_wait(th->epollfd, events, MAX_EVENT_NUMBER, timeout);
if(number < 0 && errno != EAGAIN)
{
printf("timer epoll failure\n");
break;
}
if(!number)
//超時返回,說明堆頂定時器定時到期,故執行定時事件
//並修改超時參數
{
timeout = th->tick();
}
else
//正常返回,說明有插入/刪除定時器事件發生,故重新確定定時時長,並修改超時參數
//此管道[0]端可省略讀數據操作,不影響[1]端往管道寫入新的數據
//由於[0]端設爲ET模式,一旦有新的數據寫入,epoll會且僅會通知一次
{
//如果堆中還有定時器,則定時時長等於,堆頂定時器到期的絕對時刻與當前時刻之差
//否則超時參數設爲-1,令epoll_wait阻塞
timeout = th->hsize ? th->min_heap[1]->expire - time(NULL) : -1;
}
//秒到毫秒的轉換
if(timeout != -1)
timeout *= 1000;
}
pthread_exit(NULL);
}
public:
time_heap():hsize(0), epollfd(epoll_create(5))
//默認構造函數
{
min_heap.resize(INIT_SIZE); //堆的容量初始化
//將通信管道設爲全雙工模式
int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret == 0);
//創建線程,開始監聽插入或刪除定時器事件的發生
pthread_t tid;
ret = pthread_create(&tid, NULL, run, this);
assert(ret == 0);
}
th_timer<T>* add_timer(int timeout, T *user, void (*cb_func)(T*))
//用戶接口,往時間堆中插入一個新的定時器
{
if(timeout <= 0)
{
return NULL;
}
check_size(); //確保時間堆容量足夠
//創建定時器
th_timer<T>* timer = new th_timer<T>(timeout + time(NULL), user, cb_func);
//插到堆的末尾,並開始上移操作
min_heap[++hsize] = timer;
move_up(hsize);
if(timer == min_heap[1])
//新插入的定時器成爲新的堆頂,因此定時時長可能發生變化,故通知監聽線程
{
char msg = 1;
send(pipefd[1], (char*)&msg, sizeof(msg), 0);
}
return timer;
}
void del_timer(th_timer<T> *timer)
//用戶接口,給定定時器指針,從堆中刪除此定時器
{
if(!timer)
{
return;
}
int loc = timer->loc;
swap(loc, hsize); //將此定時器與堆尾定時器交換位置
hsize--;
move_down(loc); //此處元素可能變大,因此從此處開始下移操作
delete timer;
if(loc == 1) //被刪除的定時器是原堆頂,因此定時時長可能發生變化,故通知監聽線程
{
char msg = 1;
send(pipefd[1], &msg, sizeof(msg), 0);
}
}
void set_timer(th_timer<T> *timer, int timeout)
//用戶接口,修改一個定時器的到期時間
{
if(!timer || timeout <= 0)
{
return;
}
int old_expire = timer->expire; //記錄舊到期時間
timer->expire = timeout + time(NULL); //修改到期時間
if(old_expire < timer->expire)
//到期時間增加了,則此處元素變大,故開始下移操作
{
move_down(timer->loc);
}
else if(old_expire > timer->expire)
//到期時間減小了,則此處元素變小,故開始上移操作
{
move_up(timer->loc);
}
//簡化判斷,一律認爲定時時長可能發生變化,故通知監聽線程
char msg = 1;
send(pipefd[1], &msg, sizeof(msg), 0);
}
int tick()
//堆頂定時器到期,執行定時事件,並返回新的定時時長
{
time_t cur_time = time(NULL); //記錄當前時間
th_timer<T> *top = min_heap[1]; //獲得堆頂定時器
do
{
top->cb_func(top->user_data); //執行定時事件
del_timer(top); //刪除此定時器
if(hsize > 0)
//若堆中還有定時器,則繼續獲得堆頂定時器
{
top = min_heap[1];
}
else
{
break;
}
}while(top->expire <= cur_time); //不僅處理堆頂定時器,還處理所有已到期的定時器
if(hsize == 0)
//堆中無定時器,令epoll_wait阻塞
{
return -1;
}
else
//確定新的定時時長,即堆頂定時器到期的絕對時刻與當前時刻之差
{
return top->expire - cur_time;
}
}
};
#endif