如何实现一个定时器
在以前的一次面试中,我被问到该如何实现一个定时器,当时我并没有思考过这个问题,所以回答的并不令人满意。在日常的开发中,一个需要定时执行的任务是很常见的。我认为这个问题比较有意义,也非常具有代表性。于是,我查阅了一些资料,并整理出这篇博客。
使用linux信号触发定时任务
可以使用操作系统API预订一个 SIGALRM
信号,在指定的时间后程序就能收到信号,在信号的处理函数中就能实现定时任务。具体的系统API如下:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
这里值得一提的是另一个信号 SIGVTALRM
,可以使用 setitimer
系统接口进行设置。这个定时器计算的是程序在用户模式(user mode)消耗的时间。另外,这个定时器还可以通过传入的 struct itimerval
设置为循环触发。
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
需要 注意 的是这两个API在同时使用时可能会相互干扰,它们可能会共用同一个定时器。
通过timerfd实现定时任务
timerfd是linux独有的接口,可以在定时器超时后通过一个文件描述符通知程序。
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
int timerfd_settime(int fd, int flags,
const struct itimerspec *new_value,
struct itimerspec *old_value);
int timerfd_gettime(int fd, struct itimerspec *curr_value);
当这个文件描述符变为可读(readable)时,说明定时器超时了。最方便的是,这个文件描述符可以通过类似select的多路复用接口管理,这样我们就能同时设置并等待多个定时器了。
在网络编程中借助事件的等待间隔执行定时任务
在网络编程中,通常都会用到一套IO复用接口,如select/poll/epoll等,它们会帮你同时检查多个socket上是否有读写操作。这些接口都有一个等待时间,在这个指定时间是最大等待时间,即使在没有需要处理的IO操作时也会返回。这个时间是一般是个固定值,如10ms/20ms/50ms,它被称作网络库的基准频率。在这段空余时间中可以检查定时器是否超时,并执行超时定时器的回调函数。
伪代码示例如下:
def update_events(milisec = 10):
result = selector.select(milisec)
for fd, event in result:
do something with socket event
current = time.time()
update_timer(current)
while 1:
WAIT_MILLISEC = 10
update_events(WAIT_MILLISEC)
在两次select的间隔中可以进行一些定时操作,关键就在 update_timer
中的实现:
def update_timer (current):
for timer in available_timers:
while current >= timer.expires:
timer.callback(current)
timer.expires += timer.period
available_timers
记录着当前所有的定时器, timer.expires
是他们需要被触发的时间,如果当前时间大于等于这个 timer.expires
,认为该定时器触发了。 注意 timer.expires
更新的时候是 += period
,而不是 = current + period
,后者会导致误差积累,长时间运行后偏差会越来越大。同时这里需要 while
,因为定时器的回调函数在执行时可能跨越两个以上周期,当然只触发一次的就不需要了。
可以将定时器存在一个优先队列中,每次从队列的前面取出定时器进行超时判断,直到出现了一个没超时的就终止判断,这样可以避免每次都扫描全部定时器。
在所有定时器的超时时间都相同时,可以将它们存放在一个单向队列中,每次增加时都放在最后,并从前面开始检查超时,这样只需要检查队列头部的那部分定时器了。
另一种思路是动态调整等待的间隔,每次选出最先需要超时的定时器,计算它的超时时间,将这个时间作为select的等待时间。这样每次时间都不一样,每次都基本准确,同时不会占用多余的CPU,这种模式叫做 TICKLESS
。