怎样实现一个Timer

怎样实现一个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();    【3return (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队列的每一个计时器的内容。