2 定时器

定时器是服务端开发比较常用的一个功能,但C++的标准库中,并没有提供一个定时器,本节我们创建一个简易定时器。

1、需求分析

定时器应该可以实现一次性定时器,在指定时间之后,直接回调函数的功能,其调用形式如下:

Timer.NewTimer(300, [](){
   LogInfo("call this func after 300ms"); 
});

我们还希望定时器可以在A函数中添加,在B函数中删除

bool SetTimer(Timer& timer); // 添加定时器
bool KillTimer(Timer& timer); // 删除定时器
bool OnTimer(Timer& timer);  // 执行定时器

为了识别是否是同一个定时器,我们需要定义一些Timer的属性用于比较,可以设计结构如下

struct Timer{
	int    nType;    //定时器类型
	void * nOwner;   //定时器所有者
	time_t tExpTime; //到期时间
	int    nDataLen; //后面跟的数据长度
}

判断一个定时器是否到期,需要有一个线程,不断轮询。我们可以定义轮询周期为50ms,这样一个定时器到期后,最多再过50ms就可以检测到。通过优化,精度可以进一步提高。

定期器到期后,执行其回调函数,最好是新起一个线程执行,或者将其交给一个执行任务的线程池。本节为实现简便,直接在定时器线程内执行回调函数。

2、定时器实现

先实现Timer类

Timer类添加了上文提到的四个成员变量,同时预留一个Reserve变量,用于以后改进扩充。

nDataLen表示Timer后面接的数据长度,这样以后方便实现timer的派生类,根据数据长度确定一个Timer的边界。这样基类处理timer的时候,可以做好timer的拷贝,而不用关心派生timer的类型。

Timer实现了Copy函数,在后面有数据的情况下,要一起复制拷贝。

SetTimerMilli和SetTimer是两个便利函数,用于设置定时器时间,函数实现是直接计算出过期时间,赋值到tExpTime变量。

NewTimer做了一个静态函数,方便快速实现一个一次性定时器,到期后直接回调参数传入的回调函数。

Timer的管理类实现如下

管理类有四个成员变量。

m_listTimer用于保管所以的Timer对象,使用list是因为保存的timer需要按照到期时间内排序,所以先排除了map和unordered_map;定时器到期后经常会从头部删除定时器,因此不选择vector。模版类型使用CHYTimer的指针,而不是CHYTimer类型,是因为stl容器在push的时候会拷贝一份元素,使用指针会减少拷贝开销。

m_mapFunc 用于保存timer对应的回调函数,一般用于保存一次性回调函数。没有设置一次性回调函数的,会使用默认回调函数m_funcDefaultCB。

m_lockListTimer用于保护成员数据m_listTimer和m_mapFunc。因为定时器可能在任意线程调用,多线程安全需要加锁保证。

TimerMgr产生一个全局对象,初始化时启动一个线程检测是否有到期定时器。

线程主要逻辑是:

定时器在插入的时候进行排序,因此整个定时器list是有序的。所以每次检测时,只检测第一个定时器是否到期即可。定时器到期后执行回调,删除定时器后不进行休眠,立即进行下一次检测,这样,如果有几个定时器同时到期,按列表顺序逐个处理即可,直到定时器列表为空,活着下一个定时器没有到期。

添加删除也很简单。

添加时注意拷贝一份新的Timer存储到list中,避免调用方执行完毕数据回收;添加时回调函数func可以是nullptr,这里不用管,直接赋值到map中。添加完成后,需要进行排序。这里的排序规则是,到期时间靠前的,排在前面,检测时只检测第一个。

删除时根据owner和type识别是否是同一个timer,目前实现仅删除距离到期最近的一个定时器,在添加定时器的时候,没有做查重,所以,相同的owner和type可以有多个定时器,管理类不做限制。

删除timer时需要从timer list和func map中同时删除,由于map存储的key是timer的指针,因此,owner和type相同的两个timer,其指针也不相同,不会造成误删回调函数。

最后,删除时要将timer分配的空间删除,调用free,对应添加timer时的new。

最后时执行回调部分。

优先从函数map中取出定时器对应的回调函数,若添加时未定义回调,则判断是否存在默认回调函数,并使用默认回调函数。

此处处理是在定时器的检测线程执行的,如果回调函数中的逻辑比较耗时,会影响定时器的精度,导致后续定时器延迟。

改进方式是使用任务线程池,将到期的定时器交给线程池处理,可以提高定时器的执行效率。目前简易版本,先不增加线程,后续有时间再优化。

3、定时器使用

定时器基本功能已经实现,可以有两种调用方式

第一种一次性调用,传入定时器回调函数,定时器到期后执行回调函数,结合日志输出的时间可以看到,定时器已经生效

第二种将定时器设置为CServiceBase基类函数,以后的派生服务可以重写函数

定义一个20秒的定时器,调用代码如下

输出日志为

Last updated