在Reactor的基础之上,muduo又添加了定时器的功能。有了定时器,我们就可以将内部定时器事件或者编程者在程序中给定的一定时刻需要执行的任务,就是EVENTLOOP中的runAt run After runEvery等 与 外部的IO事件一起处理。
muduo的定时器功能主要由三个class实现:Timerid Timer TimerQueue,编程用户只能看见TimerId,其他两个类为内部实现。
在这个muduo中,所有的定时器事件都是共享同一个timerfd和同一个channel,有timerqueue统一管理,因此用timerID来标识不同的定时器,用timer记录每个定时器的固有属性的自己的特值。
是一个标识符类,对用户可见,用户可以用它取消定时器
这个类定义了一些定时器的固有属性,包括定时器配套的回调函数,下一次的超时时刻,是否为重复定时器,若为重复定时器则每次超时的时间间隔是多少,已经当前定时器的序号,当前已经创建的定时器。
代码以及注释如下
const TimerCallback callback_; //定时器回调函数
Timestamp expiration_; //下一次的超时时刻
const double interval_; //超时时间间隔,如果是一次性定时器,该值为0
const bool repeat_; //是否重复(false 表示是一次性定时器)
const int64_t sequence_; //定时器序号
static AtomicInt64 s_numCreated_; //定时器计数 当前已经创建的定时器数量
TimerQueue是一个定时器管理类,主要是管理当前所有的定时器,主要的功能就是,需要高效的组织目前尚未到期的Timer,能够快速的根据当前的时间找到已经到期的定时器,还要能高效的添加和删除定时器。它将所有的定时器封装在一个事件内,对应于一个timerfd和一个channel。量级上就是相当于一个IO事件的量级。
对于组织的方案有两种:
第一种:二叉堆组织优先队列(libevent用的是高效的4-heap)
留坑后续读一下libevent再来描述,由于c++标准库的makeheap不能高效删除heap中的某个元素,因此muduo没有采用这种方法。
第二种:使用二叉搜索树(例如std::set/std::map) 将定时器按照到期顺序排序
muduo采用这种方法,设计时,一开始想到的是map,map的话就是<到期时间,定时器>,但是随之而来的问题是,假如到期时间一样怎么办,这在map中是不允许的,因此我们就让key做了一些变化,可以采用<到期时间,定时器>,并且直接用set就可以进行自动的排序。
数据结构选择好了,采用的是set,以下是源代码中的部分
timers和acitvetimers保存的是相同的数据 timers是按到期时间排序 activetimers是按照对象地址排序
//unique_ptr是c++11标准所有权的智能指针 无法得到同一对象的第二个指针
//但可以进行移动构造与移动赋值操作.
typedef std::pair<Timestamp, Timer*> Entry;//一个到期时间+定时器
typedef std::set<Entry> TimerList;
typedef std::pair<Timer*, int64_t> ActiveTimer;
typedef std::set<ActiveTimer> ActiveTimerSet;
采用数据结构对定时器进行组织以后,TimerQueue还将每个定时器timerfd_与一个Channel对应,用channel来观察timerfd_上的readable事件。
再看一下这个类的类图
TimerQueue有两个基本的功能,分别是addTimer和cancel可以增加定时器和取消定时器,
addTimerinLoop 和 cancelinLoop函数是仅能在本IO线程内调用,不会跨线程,不涉及多线程问题,主要是完成修改内部定时器列表,。getExpired得到超时的定时器列表。在handleread里面遍历超时数组,挨个调用定时器回调函数。reset是重置超时的冲虚定时器。
下面看一下定时器处理读事件的时序图
整个流程就是:EventLoop中调用loop函数 ==> 然后loop里面调用poll ==> 假设现在有定时器超时,发生timeout ==>poll返回activechannels ==> 在eventloop主循环中调用activechannel对应Channel的handleEvent ==> 再调用TimerQueue的handleread ==> 调用getExpired得到超时的定时器,找到对应的回调函数,执行!(这里onTimer是用户层定义的回调函数,在timerqueue的构造函数里面设置channel的回调函数为handleread)。
再进一步的看,相当于timerqueue将所有的定时器组织到一起,用一个timerfd和一个channel来与eventloop交互,相当于一个事件。