怎样实现一个Timer(计时器)。
1. libuv Timer
在之前的文章里面介绍过libuv的Timer使用,在libuv中Timer超时的判断是在主循环中实现的。
# 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相关的一些逻辑。【1】处在每次循环的开始处,会重新获得当前时间,并将其赋值给loop->time。【2】处处理到期的Timer。【3】处会计算即将到期的Timer离现在时间还有多少,此时间将会作为poll的超时时间。
2. Libtask
Libtask里面没有完全意义上的Timer,这里分析的是taskdelay方法,通过taskdelay方法可以实现一个别样的Timer。
void
timertask(void* v) {
taskdelay((int)v); // 延迟v毫秒
... ... // do what you will do
}
taskdelay不是真正意义上的Timer,但它的实现却和Timer的实现思路很相似,因此向拿出来分析一下。先看一下taskdelay的内部逻辑。
uint
taskdelay(uint ms)
{
uvlong when, now;
Task *t;
if(!startedfdtask){
startedfdtask = 1;
// fdtask只创建一次
taskcreate(fdtask, 0, 32768); 【1】
}
now = nsec();
when = now+(uvlong)ms*1000000;
// 找到第一个终止时间大于when的task
for(t=sleeping.head; t!=nil && t->alarmtime < when; t=t->next)
;
// 将当前task插入队列 【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后面会介绍,其实是调度的主循环的task,如果fdtask没有创建,首先进行创建。之后我们会根据when将其插入sleeping队列中,sleeping使用列表来维护,最小堆当然是更好的选择。之后,在【3】处我们进行taskswitch,切换到fdtask。
void
fdtask(void *v)
{
... ...
for(;;){
/* let everyone else run */
// 让其他的Task先运行一下下
while(taskyield() > 0) 【1】
;
... ...
// 计算poll的超时时间
if((t=sleeping.head) == nil)
ms = -1; // 如果sleeping队列为空,poll没有超时时间
else{
/* sleep at most 5s */
now = nsec();
if(now >= t->alarmtime)
ms = 0; // 如果已经有超时timer,poll立马返回
else if(now+5*1000*1000*1000LL >= t->alarmtime)
ms = (t->alarmtime - now)/1000000;
else
ms = 5000;
}
// ms 很重要,超时后有些时间到期的任务就可以触发起来了
if(poll(pollfd, npollfd, ms) < 0){
... ...
}
... ...
now = nsec();
// 满足sleep的任务可以触发了
while((t=sleeping.head) && now >= t->alarmtime){
deltask(&sleeping, t);
if(!t->system && --sleepingcounted == 0)
taskcount--;
taskready(t);
}
}
}
一开始使用taskyield让其他在就绪列表的任务都提前运行一下。之后计算poll的超时时间。在最后将超时的timer对应的Task加入就绪列表。是不是和前面处理的Timer逻辑十分相似!
3.mudou
陈硕写的《Linux多线程服务端编程》一书中也提到了一种Timer的实现方法,感觉也是很巧妙的。
他使用timerfd_create(2)/timerfd_settime(2)/timerfdgettime(2),如下是陈硕对使用timerfd*的解释:
timerfd_create(2)把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,这样就能很方便地融入select(2)/poll(2)框架中,用统一的方式来处理IO事件和超时事件,这也正式Reactor模式的长处。
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创建一个文件描述符,将其加入到poll中,通过timerfd_settime方法来修改时间,但需要注意的是这个接口只适用于Linux系统下,并不通用。
4.总结
可以看出Timer的实现并不是很复杂,框架会利用poll(epoll/kqueue)来实现一个主tick,计算最先超时的Timer的时间作为poll的超时时间,当poll超时的时候会处理Timer队列的每一个计时器的内容。