原文地址:http://whosemario.github.io/2015/11/12/timer/
怎樣實(shí)現(xiàn)一個(gè)Timer(計(jì)時(shí)器)。
1. libuv Timer
在之前的文章里面介紹過libuv的Timer使用,在libuv中Timer超時(shí)的判斷是在主循環(huán)中實(shí)現(xiàn)的乾戏。
# in core.c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
... ...
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop); 【1】
uv__run_timers(loop); 【2】
... ...
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop); 【3】
uv__io_poll(loop, timeout);
... ...
}
這里我只列出和Timer相關(guān)的一些邏輯爸邢「盥簦【1】處在每次循環(huán)的開始處宴杀,會(huì)重新獲得當(dāng)前時(shí)間茅郎,并將其賦值給loop->time蜗元。【2】處處理到期的Timer系冗∞瓤郏【3】處會(huì)計(jì)算即將到期的Timer離現(xiàn)在時(shí)間還有多少,此時(shí)間將會(huì)作為poll的超時(shí)時(shí)間掌敬。
2. Libtask
Libtask里面沒有完全意義上的Timer惯豆,這里分析的是taskdelay方法,通過taskdelay方法可以實(shí)現(xiàn)一個(gè)別樣的Timer奔害。
void
timertask(void* v) {
taskdelay((int)v); // 延遲v毫秒
... ... // do what you will do
}
taskdelay不是真正意義上的Timer楷兽,但它的實(shí)現(xiàn)卻和Timer的實(shí)現(xiàn)思路很相似,因此向拿出來分析一下华临。先看一下taskdelay的內(nèi)部邏輯芯杀。
uint
taskdelay(uint ms)
{
uvlong when, now;
Task *t;
if(!startedfdtask){
startedfdtask = 1;
// fdtask只創(chuàng)建一次
taskcreate(fdtask, 0, 32768); 【1】
}
now = nsec();
when = now+(uvlong)ms*1000000;
// 找到第一個(gè)終止時(shí)間大于when的task
for(t=sleeping.head; t!=nil && t->alarmtime < when; t=t->next)
;
// 將當(dāng)前task插入隊(duì)列 【2】
if(t){
taskrunning->prev = t->prev;
taskrunning->next = t;
}else{
taskrunning->prev = sleeping.tail;
taskrunning->next = nil;
}
t = taskrunning;
t->alarmtime = when;
if(t->prev)
t->prev->next = t;
else
sleeping.head = t;
if(t->next)
t->next->prev = t;
else
sleeping.tail = t;
// 這是做什么?
if(!t->system && sleepingcounted++ == 0)
taskcount++;
taskswitch(); 【3】
return (nsec() - now)/1000000;
}
fdtask后面會(huì)介紹,其實(shí)是調(diào)度的主循環(huán)的task揭厚,如果fdtask沒有創(chuàng)建却特,首先進(jìn)行創(chuàng)建。之后我們會(huì)根據(jù)when將其插入sleeping隊(duì)列中筛圆,sleeping使用列表來維護(hù)核偿,最小堆當(dāng)然是更好的選擇。之后顽染,在【3】處我們進(jìn)行taskswitch漾岳,切換到fdtask。
void
fdtask(void *v)
{
... ...
for(;;){
/* let everyone else run */
// 讓其他的Task先運(yùn)行一下下
while(taskyield() > 0) 【1】
;
... ...
// 計(jì)算poll的超時(shí)時(shí)間
if((t=sleeping.head) == nil)
ms = -1; // 如果sleeping隊(duì)列為空粉寞,poll沒有超時(shí)時(shí)間
else{
/* sleep at most 5s */
now = nsec();
if(now >= t->alarmtime)
ms = 0; // 如果已經(jīng)有超時(shí)timer尼荆,poll立馬返回
else if(now+5*1000*1000*1000LL >= t->alarmtime)
ms = (t->alarmtime - now)/1000000;
else
ms = 5000;
}
// ms 很重要,超時(shí)后有些時(shí)間到期的任務(wù)就可以觸發(fā)起來了
if(poll(pollfd, npollfd, ms) < 0){
... ...
}
... ...
now = nsec();
// 滿足sleep的任務(wù)可以觸發(fā)了
while((t=sleeping.head) && now >= t->alarmtime){
deltask(&sleeping, t);
if(!t->system && --sleepingcounted == 0)
taskcount--;
taskready(t);
}
}
}
一開始使用taskyield讓其他在就緒列表的任務(wù)都提前運(yùn)行一下唧垦。之后計(jì)算poll的超時(shí)時(shí)間捅儒。在最后將超時(shí)的timer對(duì)應(yīng)的Task加入就緒列表。是不是和前面處理的Timer邏輯十分相似振亮!
3.mudou
陳碩寫的《Linux多線程服務(wù)端編程》一書中也提到了一種Timer的實(shí)現(xiàn)方法巧还,感覺也是很巧妙的。
他使用timerfd_create(2)/timerfd_settime(2)/timerfd_gettime(2),如下是陳碩對(duì)使用timerfd_*的解釋:
timerfd_create(2)把時(shí)間變成了一個(gè)文件描述符坊秸,該“文件”在定時(shí)器超時(shí)的那一刻變得可讀麸祷,這樣就能很方便地融入select(2)/poll(2)框架中,用統(tǒng)一的方式來處理IO事件和超時(shí)事件褒搔,這也正式Reactor模式的長(zhǎng)處阶牍。
int createTimerfd()
{
int timerfd = ::timerfd_create(CLOCK_MONOTONIC,
TFD_NONBLOCK | TFD_CLOEXEC);
if (timerfd < 0)
{
LOG_SYSFATAL << "Failed in timerfd_create";
}
return timerfd;
}
通過timerfd_create創(chuàng)建一個(gè)文件描述符,將其加入到poll中,通過timerfd_settime方法來修改時(shí)間星瘾,但需要注意的是這個(gè)接口只適用于Linux系統(tǒng)下走孽,并不通用。
4.總結(jié)
可以看出Timer的實(shí)現(xiàn)并不是很復(fù)雜琳状,框架會(huì)利用poll(epoll/kqueue)來實(shí)現(xiàn)一個(gè)主tick磕瓷,計(jì)算最先超時(shí)的Timer的時(shí)間作為poll的超時(shí)時(shí)間,當(dāng)poll超時(shí)的時(shí)候會(huì)處理Timer隊(duì)列的每一個(gè)計(jì)時(shí)器的內(nèi)容念逞。