目录
muduo中定时器模块的特点
- 整个TimerQueue只使用一个timerfd来观察定时事件,并且每次重置timerfd时只需跟set中第一个节点比较即可,因为set本身是一个有序队列。
- 整个定时器队列采用了muduo典型的事件分发机制,可以使的定时器的到期时间像fd一样在Loop线程中处理。
- 之前Timestamp用于比较大小的重载方法在这里得到了很好的应用。
muduo中的定时器系统
在muduo的定时器系统中,一共由四个类:Timestamp,Timer,TimeId,TimerQueue组成。其中最关键的是Timer和TimerQueue两个类。该项目中没有使用Timeld类。
TimerQueue类,是整个定时器设施的核心,其他三个类简介其作用。 其中Timestamp是一个以int64_t表示的微秒级绝对时间,而Timer则表示一个定时器的到时事件,是否具有重复唤醒的时间等,TimerId表示在在TimerQueue中对Timer的索引。
muduo中定时器实现的逻辑:
①:Timer类:Timer类包含了一个超时时间戳和一个回调函数。当超时时间戳到达时,调用回调函数出发定时事件。
②:TimerQueue类:TimerQueue类是一个基于事件戳排序的定时器容器。它使用了最小堆(MinHeap)数据结构来保证定时器按照超时时间的顺序进行排列。TimerQueue类提供了添加、删除和获取最近超时的定时器的接口。
③:EventLoop类:EventLoop类是muduo网络库的核心组件,负责事件的循环和处理。其中包括定时器事件的管理。EventLoop有一个成员变量TimerQueue timerQueue_,用于存储定时器对象。EventLoop会在事件循环中监测定时器队列中最近超时的定时器,并调用其回调函数。
Timer类
- 定时器到期后需要回调函数;
- 定时器需要记录我们的超时时间;
- 如果是重复事件(比如每间隔5秒扫描一次用户连接),我们还需要记录超时间间隔;
- 对应的,我们需要一个bool类型的值标注这个定时器是一次性的还是重复的。
对于不是一次性的定时器,我们通过restart方法,观察定时器的构造函数中
repeat_(interval > 0.0) // 一次性定时器设置为0
如果是需要重新利用的定时器,会调用restart方法,我们设置其下一次超时时间为「当前时间 + 间隔时间」。如果是「一次性定时器」,那么就会自动生成一个空的 Timestamp,其时间自动设置为 0.0
。
TimerQueue类
TimerQueue类管理作为管理定时器的结构。其内部使用 STL 容器 set 来管理定时器。我们以时间戳作为键值来获取定时器。set 内部实现是红黑树,红黑树中序遍历便可以得到按照键值排序过后的定时器节点。小顶堆,说明最小的事件戳(即最早出发的定时器位于容器顶部)
using Entry = std::pair<Timestamp, Timer*>;
using TimerList = std::set<Entry>;
①:整个TimerQueue之打开一个timefd,用以观察定时器队列队首的到期事件。其原因是因为set容器是一个有序队列,以<排序,就是说整个队列中,Timer的到期时间时从小到大排列的,正是因为这样,才能做到节省系统资源的目的。
②:整个定时器队列采用了muduo典型的事件分发机制,可以使得定时器的到期时间像fd一样在Loop线程中处理。
使用timerfd实现定时功能
timerfd与IO多路复用机制(如epoll)结合使用,可以实现基于事件得事件驱动编程。当定时器到期时,可以通过epoll等机制监视timerfd得可读事件,并触发相应得事件处理逻辑。当超时事件发生时,该文件描述符就变为可读。
timer_create
timerfd_create(CLOCK_MONOTONIC,TFD_NONBLOCK | TFD_CLOEXEC);
timerfd_settime
int timerfd_settime(int fd,int flags
const struct itimerspec *new_value
struct itimerspec *old_value);
//成功返回0
我们使用此函数启动或停止定时器。
创建TimerQueue
- 通过timer_create创建timerfd
- TimerQueue类初始化后,设置其timerfdChannel绑定读事件,并置于可读状态
删除定时器管理对象
插入定时器流程
1、EventLoop调用方法,加入一个定时器事件,传入定时器回调函数,超时时间和间隔时间(为0.0则为一次性定时器),addTimer方法根据这些属性构造新得定时器。
2、定时器队列内部插入此定时器,并判断这个定时器得超时时间是否比先前得都早。如果是最早触发的,就会调用resetTimerfd
方法重新设置tiemrfd_的触发时间。内部会根据超时时间和现在时间计算出新的超时时间。
内部实现的插入方法获取此定时器的超时时间,如果比先前的时间小就说明第一个触发。那么我们会设置好布尔变量。因此这涉及到timerfd_的触发时间。
重置timerfd_ :通过计算时间差使用timerfd_settime将新的到期时间设置到指定定时器事件得文件描述符上
处理到期定时器
- EventLoop获取活跃的activeChannel,并分别调取它们绑定的回调函数。这里对于timerfd_,就是调用了handleRead方法。
- handleRead方法获取已经超时的定时器组数组,遍历到期的定时器并调用内部绑定的回调函数。之后调用
reset
方法重新设置定时器 - reset方法内部判断这些定时器是否是可重复使用的,如果是则继续插入定时器管理队列,之后自然会触发事件。如果不是,那么就删除。如果重新插入了定时器,记得还需重置timerfd_。
①:ReadTimerFd读取定时器文件描述符(timerfd)的值:
②: getExpired获取已经到期的定时器列表:
- getExpired函数接收一个时间戳参数now,表示当前时间。
- 首先,创建一个空的expired向量,用于存储已经到期的定时器节点。
- 创建一个临时的定时器节点sentry,其时间戳为now,指针为一个特殊值UINTPTR_MAX,用于作为一个哨兵节点。(这里设置所有定时器都不会超时,可以自己定义)
- 使用lower_bound函数在timers_容器中查找第一个大于或等于sentry的节点,返回一个迭代器指向该位置,存储在end中。
- 使用std::copy函数将timers_容器中从起始位置到end位置的节点复制到expired向量中。
- 使用erase函数从timers_容器中删除从起始位置到end位置的节点。(set小根堆,越小说明发生越早,所以删除前半段)
- 最后,返回存储了已删除的到期定时器节点的expired向量。
③:遍历需要删除的定时器,执行回调函数:
④:重新设置定时器之前,调用reset
函数对已到期的定时器进行处理: