传统的Reactor通过控制select和poll的等待时间来实现定时,而现在在Linux中有了timerfd,我们可以用和处理IO事件相同的方式来处理定时,代码的一致性更好。
为什么选择timerfd
常见的定时函数有如下几种:
sleep
alarm
usleep
nanosleep
clock_nanosleep
getitimer / setitimer
timer_create / timer_settime / timer_gettime / timer_delete
timerfd_create / timerfd_gettime / timerfd_settime
我们之所以选择timerfd,是因为:
1.sleep / alarm / usleep 在实现时有可能用了信号 SIGALRM,在多线程程序中处理信号是个相当麻烦的事情,应当尽量避免。
2.nanosleep 和 clock_nanosleep 是线程安全的,但是在非阻塞网络编程中,绝对不能用让线程挂起的方式来等待一段时间,程序会失去响应。正确的做法是注册一个时间回调函数。
3.getitimer 和 timer_create 也是用信号来传递超时,在多线程程序中也会有麻烦。
4.timer_create 可以指定信号的接收方是进程还是线程,算是一个进步,不过在信号处理函数(signal handler)能做的事情实在很受限。
5.timerfd_create 把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,这样就能很方便地融入到 select/poll 框架中,用统一的方式来处理 IO 事件和超时事件,这也正是 Reactor 模式的长处。
timerfd相关函数介绍:
#include <sys/timerfd.h>
/**
* 此函数用于创建一个定时器文件
* 参数clockid可以是CLOCK_MONOTONIC或者CLOCK_REALTIME
* 参数flags可以是0或者TFD_CLOEXEC/TFD_NONBLOCK
* 函数返回值是一个文件句柄fd
*/
int timerfd_create(int clockid, int flags);
/**
* 此函数用于设置新的超时时间,并开始计时
* 参数fd是timerfd_create返回的文件句柄
* 参数flags为TFD_TIMER_ABSTIME(1)代表设置的是绝对时间;为0代表相对时间
* 参数new_value为需要设置的超时和间隔时间
* 参数old_value为定时器这次设置之前的超时时间
* 函数返回0代表设置成功
*/
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
/**
* 此函数用于获得定时器距离下次超时还剩下的时间
* 如果调用时定时器已经到期,并且该定时器处于循环模式
* 即设置超时时间时struct itimerspec::it_interval不为0
* 那么调用此函数之后定时器重新开始计时
*/
int timerfd_gettime(int fd, struct itimerspec *curr_value);
itimerspec结构体;
struct itimerspec {
struct timespec it_interval; // interval for periodic timer
struct timespec it_value; // initial expiration
};
struct timespec {
time_t tv_sec; // seconds
long tv_nsec; // nano-seconds
};
muduo定时器的实现
muduo的定时器功能由三个class实现,TimerId、Timer、TimerQueue,用户只能看到第一个class,另外两个都是内部实现细节。
TimerId被设计用来取消Timer的,它的结构很简单,只有一个Timer指针和其序列号。其中还声明了TimerQueue为其友元,可以操作其私有数据。
Timer是对定时器的高层次抽象,封装了定时器的一些参数,例如超时回调函数、超时时间、超时时间间隔、定时器是否重复、定时器的序列号。其函数大都是设置这些参数,run()用来调用回调函数,restart()用来重启定时器(如果设置为重复)。
重点介绍一下TimerQueue类。
TimerQueue class
TimerQueue的接口很简单,只有两个函数addTimer()和cancel()。它的内部有channel,和timerfd相关联。添加新的Timer后,在超时后,timerfd可读,会处理channel事件,之后调用Timer的回调函数;在timerfd的事件处理后,还会检查一遍超时定时器,如果其属性为重复还会再次添加到定时器集合中。
时序图:
(1)TimerQueue数据结构的选择
TimerQueue需要高效地组织目前尚未到期的Timer,能快速地根据当前时间找到已经到期的Timer,也要能高效地添加和删除Timer。因而可以用二叉搜索树(例如std::set/std::map),把Timer按到期时间先后排好序,其操作的复杂度是O(logN),但我们使用时还要处理两个Timer到期时间相同的情况(map不支持key相同的情况),做法如下:
// 两种类型的set,一种按时间戳排序,一种按Timer的地址排序
// 实际上,这两个set保存的是相同的定时器列表
typedef std::pair<Timestamp, Timer*> Entry;
typedef std::set<Entry> TimerList;
typedef std::pair<Timer*, int64_t> ActiveTimer;
typedef std::