定时器是服务端开发比较常用的一个功能,但C++的标准库中,并没有提供一个定时器,本节我们创建一个简易定时器。
1、需求分析
定时器应该可以实现一次性定时器,在指定时间之后,直接回调函数的功能,其调用形式如下:
Copy Timer.NewTimer(300, [](){
LogInfo("call this func after 300ms");
});
我们还希望定时器可以在A函数中添加,在B函数中删除
Copy bool SetTimer(Timer& timer); // 添加定时器
bool KillTimer(Timer& timer); // 删除定时器
bool OnTimer(Timer& timer); // 执行定时器
为了识别是否是同一个定时器,我们需要定义一些Timer的属性用于比较,可以设计结构如下
Copy struct Timer{
int nType; //定时器类型
void * nOwner; //定时器所有者
time_t tExpTime; //到期时间
int nDataLen; //后面跟的数据长度
}
判断一个定时器是否到期,需要有一个线程,不断轮询。我们可以定义轮询周期为50ms,这样一个定时器到期后,最多再过50ms就可以检测到。通过优化,精度可以进一步提高。
定期器到期后,执行其回调函数,最好是新起一个线程执行,或者将其交给一个执行任务的线程池。本节为实现简便,直接在定时器线程内执行回调函数。
2、定时器实现
先实现Timer类
Copy typedef std::function<void()> TimerCBFunc;
class CHYTimer
{
public:
bool Copy(CHYTimer& timer){
nType = timer.nType;
nOwener = timer.nOwener;
tExpTime = timer.tExpTime;
nDataLen = timer.nDataLen;
if (nDataLen != 0)
{
memcpy(this+1, &timer+1, nDataLen);
}
return true;
}
void SetTimerMilli(int milli);
void SetTimer(int sec);
static void NewTimer(unsigned int millisecond, TimerCBFunc func);
public:
int nType = 0;
void* nOwener = nullptr;
time_t tExpTime = 0;
int nDataLen = 0;
int nReserve = 0;
};
Timer类添加了上文提到的四个成员变量,同时预留一个Reserve变量,用于以后改进扩充。
nDataLen表示Timer后面接的数据长度,这样以后方便实现timer的派生类,根据数据长度确定一个Timer的边界。这样基类处理timer的时候,可以做好timer的拷贝,而不用关心派生timer的类型。
Timer实现了Copy函数,在后面有数据的情况下,要一起复制拷贝。
SetTimerMilli和SetTimer是两个便利函数,用于设置定时器时间,函数实现是直接计算出过期时间,赋值到tExpTime变量。
NewTimer做了一个静态函数,方便快速实现一个一次性定时器,到期后直接回调参数传入的回调函数。
Timer的管理类实现如下
Copy typedef std::function<void(CHYTimer*)> DefaultTimerCBFunc;
typedef std::mutex Mutex;
typedef std::lock_guard<Mutex> AutoLock;
class TimerMgr
{
private:
TimerMgr();
bool Init();
public:
static TimerMgr* Instance();
public:
void SetDefaultCallBackFunc(DefaultTimerCBFunc func){m_funcDefaultCB = func;}
CHYTimer* SetTimer(CHYTimer& timer, TimerCBFunc func = nullptr);
bool KillTimer(CHYTimer& timer);
private:
unsigned long WorkThreadFunc();
bool DelTimer(CHYTimer* timer); // 从列表中删除Timer
bool DoFunc(CHYTimer* timer);
private:
std::list<CHYTimer*> m_listTimer;
Mutex m_lockListTimer;
DefaultTimerCBFunc m_funcDefaultCB = nullptr;
std::unordered_map<CHYTimer*, TimerCBFunc> m_mapFunc;
};
管理类有四个成员变量。
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产生一个全局对象,初始化时启动一个线程检测是否有到期定时器。
Copy bool TimerMgr::Init()
{
std::thread th(&TimerMgr::WorkThreadFunc, this);
th.detach();
return true;
}
线程主要逻辑是:
定时器在插入的时候进行排序,因此整个定时器list是有序的。所以每次检测时,只检测第一个定时器是否到期即可。定时器到期后执行回调,删除定时器后不进行休眠,立即进行下一次检测,这样,如果有几个定时器同时到期,按列表顺序逐个处理即可,直到定时器列表为空,活着下一个定时器没有到期。
Copy unsigned long TimerMgr::WorkThreadFunc()
{
while (true)
{
int nListSize = 0;
{
AutoLock lock(m_lockListTimer);
nListSize = m_listTimer.size();
}
if (nListSize == 0)
{
// 检测没有定时器,直接sleep 50ms再检测
std::this_thread::sleep_for(std::chrono::milliseconds(TIMER_INTERVAL_CHECK));
continue;
}
time_t tNow = GetTimeStamp();
// 取第一个定时器查看,是否到期
CHYTimer* pTimer = nullptr;
{
AutoLock lock(m_lockListTimer);
pTimer = m_listTimer.front();
}
if (pTimer)
{
if (tNow >= pTimer->tExpTime) //定时器过期,执行定时器回调,删除定时器
{
DoFunc(pTimer);
DelTimer(pTimer); // 删除这个timer
}
else // 定时器还没到,看到期时间和50ms哪个更小,sleep较小的时间
{
int nSpleep = std::min((int)(pTimer->tExpTime - tNow) / 1000, TIMER_INTERVAL_CHECK) ;
std::this_thread::sleep_for(std::chrono::milliseconds(nSpleep));
}
}
}
return 0;
}
添加删除也很简单。
添加时注意拷贝一份新的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。
Copy bool compareTimer(CHYTimer* timer1, CHYTimer* timer2)
{
return timer1->tExpTime < timer2->tExpTime;
}
CHYTimer* TimerMgr::SetTimer(CHYTimer& timer, TimerCBFunc func)
{
int nSize = sizeof(CHYTimer) + timer.nDataLen;
char* t = new char[nSize];
CHYTimer* pTimer = (CHYTimer*)t;
pTimer->Copy(timer);
{
AutoLock lock(m_lockListTimer);
m_listTimer.push_front(pTimer);
m_mapFunc[pTimer] = func;
m_listTimer.sort(compareTimer);
}
return pTimer;
}
bool TimerMgr::KillTimer(CHYTimer& timer)
{
return DelTimer(&timer);
}
bool TimerMgr::DelTimer(CHYTimer* timer)
{
AutoLock lock(m_lockListTimer);
auto itr = std::find_if(m_listTimer.begin(), m_listTimer.end(), [&](CHYTimer* pTimer){
return (timer->nOwener == pTimer->nOwener && timer->nType == pTimer->nType);
});
if (itr != m_listTimer.end())
{
CHYTimer* pTimer = (CHYTimer*)*itr;
m_listTimer.erase(itr);
m_mapFunc.erase(pTimer);
char*t = (char*)pTimer;
delete []t;
}
return true;
}
最后时执行回调部分。
优先从函数map中取出定时器对应的回调函数,若添加时未定义回调,则判断是否存在默认回调函数,并使用默认回调函数。
此处处理是在定时器的检测线程执行的,如果回调函数中的逻辑比较耗时,会影响定时器的精度,导致后续定时器延迟。
改进方式是使用任务线程池,将到期的定时器交给线程池处理,可以提高定时器的执行效率。目前简易版本,先不增加线程,后续有时间再优化。
Copy bool TimerMgr::DoFunc(CHYTimer* timer)
{
TimerCBFunc func = nullptr;
{
AutoLock lock(m_lockListTimer);
func = m_mapFunc[timer];
}
if (func)
{
func();
}
else if(m_funcDefaultCB)
{
m_funcDefaultCB(timer);
}
return true;
}
3、定时器使用
定时器基本功能已经实现,可以有两种调用方式
Copy LogInfo( "this is a message before timer" );
CHYTimer::NewTimer(13000,[](){
LogInfo( "this is a message after 13s by timer" );
});
CHYTimer::NewTimer(3000,[](){
LogInfo( "this is a message after 3s by timer" );
});
CHYTimer::NewTimer(8000,[](){
LogInfo( "this is a message after 8s by timer" );
});
第一种一次性调用,传入定时器回调函数,定时器到期后执行回调函数,结合日志输出的时间可以看到,定时器已经生效
Copy 12/01/22 13:37:11 00000000 [Info]this is a message before timer
12/01/22 13:37:14 00000000 [Info]this is a message after 3s by timer
12/01/22 13:37:19 00000000 [Info]this is a message after 8s by timer
12/01/22 13:37:24 00000000 [Info]this is a message after 13s by timer
第二种将定时器设置为CServiceBase基类函数,以后的派生服务可以重写函数
Copy void CServiceBase::SetTimer(CHYTimer * pTimer)
{
TimerMgr::Instance()->SetTimer(*pTimer);
}
void CServiceBase::KillTimer(CHYTimer * pTimer)
{
TimerMgr::Instance()->KillTimer(*pTimer);
}
void CServiceBase::OnTimer(CHYTimer * pTimer)
{
switch (pTimer->nType)
{
case 1:
LogInfo("this is Service Timer " );
break;
default:
break;
}
}
定义一个20秒的定时器,调用代码如下
Copy CHYTimer timer;
timer.SetTimer(20);
timer.nType = 1;
SetTimer(&timer);
输出日志为
Copy 12/01/22 13:43:28 00000000 [Info]ServiceBase Run ...
12/01/22 13:43:48 00000000 [Info]this is Service Timer