对定时器概念不是特别熟悉的同学可以先看看 定时器概述 这篇文章。
地址: https://blog.youkuaiyun.com/qq492927689/article/details/123262563
下面我们从代码层面上,讲解一下相对定时器如何实现。
先上代码:
main.cpp:
#include "Timer.h"
void TimerOut(TIMER* pTimer, void* pParam) {
printf("TimerOut = %s\n", (char*)pParam);
}
void TimerOut2(TIMER* pTimer, void* pParam) {
printf("TimerOut2 = %s\n", (char*)pParam);
Kill_Timer(pTimer);
}
void TimerOut3(TIMER* pTimer, void* pParam) {
int nCount = (int)pParam;
printf("TimerOut3 = %d\n", nCount);
if (nCount++ == 10) {
EventExit(pTimer->pEvent);
}
pParam = (void*)nCount;
}
int main() {
EVENTOBJECT* pEvent = EventCreate();
TIMER* pTimer1 = Set_Timer(pEvent, TimerOut, 1000, (void*)"pTimer1");
TIMER* pTimer2 = Set_Timer(pEvent, TimerOut2, 2000, (void*)"pTimer2");
TIMER* pTimer3 = Set_Timer(pEvent, TimerOut3, 3000, (void*)1);
EventDispatch(pEvent);
EventDestory(pEvent);
return 0;
}
timer.h
#ifndef __TIMER_H__
#define __TIMER_H__
#include <windows.h>
#include <list>
struct __TIMER;
typedef std::list<struct __TIMER*> TIMERLIST;
class EVENTOBJECT {
public:
TIMERLIST RegList;//等待注册的队列
TIMERLIST WaitList;//已经注册,等待激活的队列
TIMERLIST ActiveList;//已经激活的队列
HANDLE hEvent;//用来进行等待的内核对象
DWORD dwCurTime;//当前系统时间
#define EVENTSTATE_RUN 0
#define EVENTSTATE_EXIT 1
DWORD dwState;//当前状态
};
typedef void (*TimerProc)(struct __TIMER*, void* pParam);
typedef struct __TIMER {
DWORD dwTimeOut;//超时间隔
DWORD dwTime;//超时时间
TimerProc pfnFunc;//触发的回调函数
void* pParam;//定时器参数
EVENTOBJECT* pEvent;
}TIMER;
EVENTOBJECT* EventCreate(); //创建对象
int EventDispatch(EVENTOBJECT* pEvent); //循环等待超时和执行超时实践
BOOL EventExit(EVENTOBJECT* pEvent); //退出循环
void EventDestory(EVENTOBJECT* pEvent); //销毁对象
TIMER* Set_Timer(EVENTOBJECT* pEvent, TimerProc pfnTimerProc, DWORD dwTimeOut, void* pParam);
void Kill_Timer(TIMER* pTimer);
//下面这些函数,是属于私有函数,一般不应该公开接口的
//注冊定時器
BOOL timer_register(EVENTOBJECT* pEvent);
//按從小到大的順序插入到注冊完成隊列中
BOOL timer_insert_waitlist(TIMER* pTimer);
//获取最小的超时时间
DWORD timer_get_min_timtout(EVENTOBJECT* pEvent);
//检查有多少定时器被激活,并将它们移动到激活队列中
int timer_check_active(EVENTOBJECT* pEvent);
//轮流执行激活队列的回调函数
void timer_active(EVENTOBJECT* pEvent);
//更新当前时间的缓存
void update_curtime(EVENTOBJECT* pEvent);
#endif
timer.cpp
#include "Timer.h"
EVENTOBJECT* EventCreate() {
HANDLE hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
DWORD dwError = GetLastError();
if (NULL == hEvent)
{
return NULL;
}
EVENTOBJECT* pEvent = new EVENTOBJECT;
pEvent->hEvent = hEvent;
pEvent->dwCurTime = 0;
pEvent->dwState = EVENTSTATE_RUN;
return pEvent;
}
BOOL EventExit(EVENTOBJECT* pEvent) {
if (EVENTSTATE_RUN != pEvent->dwState) {
return FALSE;
}
pEvent->dwState = EVENTSTATE_EXIT;
//可能已經
return SetEvent(pEvent->hEvent);
}
void EventDestory(EVENTOBJECT* pEvent) {
if (pEvent->hEvent) {
::CloseHandle(pEvent->hEvent);
}
for (TIMERLIST::iterator it = pEvent->RegList.begin();
pEvent->RegList.end() != it; it++) {
delete(*it);
}
pEvent->RegList.clear();
for (TIMERLIST::iterator it = pEvent->ActiveList.begin();
pEvent->ActiveList.end() != it; it++) {
delete(*it);
}
pEvent->ActiveList.clear();
for (TIMERLIST::iterator it = pEvent->WaitList.begin();
pEvent->WaitList.end() != it; it++) {
delete(*it);
}
pEvent->WaitList.clear();
delete pEvent;
}
int EventDispatch(EVENTOBJECT* pEvent) {
DWORD dwTimeOut = -1;
pEvent->dwState = EVENTSTATE_RUN;
while (EVENTSTATE_RUN == pEvent->dwState) {
//注册到WaitList中
timer_register(pEvent);
//找出最小的超时时间
dwTimeOut = timer_get_min_timtout(pEvent);
ResetEvent(pEvent->hEvent);
//等待超时
DWORD dwError = ::WaitForSingleObject(pEvent->hEvent, dwTimeOut);
//将超时的Timer放到ActiveList中
timer_check_active(pEvent);
//将ActiveList的元素,逐个调用回调函数
timer_active(pEvent);
}
return 0;
}
TIMER* Set_Timer(EVENTOBJECT* pEvent, TimerProc pfnTimerProc, DWORD dwTimeOut, void* pParam) {
TIMER* pTimer = new TIMER;
pTimer->dwTimeOut = dwTimeOut;
pTimer->pEvent = pEvent;
pTimer->pfnFunc = pfnTimerProc;
pTimer->pParam = pParam;
pTimer->dwTime = 0;
//注意,这里只是将定时器对象放到等待注册的队列,注册实际上还没执行
pEvent->RegList.push_back(pTimer);
//如果set_timer函数不再主线程中,主线程可能正在wait,所以唤醒一下
SetEvent(pEvent->hEvent);
return pTimer;
}
void Kill_Timer(TIMER* pTimer) {
EVENTOBJECT* pEvent = pTimer->pEvent;
//就是把它从容器中删除,没人管理这个定时器对象,它就无效了
//跟set_timer不一样,这个函数不需要 SetEvent
for (TIMERLIST::iterator it = pEvent->RegList.begin();
pEvent->RegList.end() != it; it++) {
if (*it == pTimer) {
pEvent->RegList.erase(it);
delete(pTimer);
return;
}
}
for (TIMERLIST::iterator it = pEvent->ActiveList.begin();
pEvent->ActiveList.end() != it; it++) {
if (*it == pTimer) {
pEvent->ActiveList.erase(it);
delete(pTimer);
return;
}
}
for (TIMERLIST::iterator it = pEvent->WaitList.begin();
pEvent->WaitList.end() != it; it++) {
if (*it == pTimer) {
pEvent->WaitList.erase(it);
delete(pTimer);
return;
}
}
}
BOOL timer_register(EVENTOBJECT* pEvent) {
//先更新一下时间
update_curtime(pEvent);
//估计会有同学好奇,为什么不直接在SetTimer函数里面直接进行实时注册,而要放入RegList
//队列中异步注册,有种脱了裤子放屁的感觉。其实这样做是可以提升性能。
//首先一个是可以减少获取时间的函数调用,然后是统一注册时间,往往也会统一了被激活的
//时间,性能会有很大提升的。而且代码执行放在同一个地方,管理起来也比较方便
//当然项目中还得按实际情况来,这里只是提供一种思路给大家参考
TIMERLIST::iterator it;
for (it = pEvent->RegList.begin(); pEvent->RegList.end() != it; it++) {
timer_insert_waitlist(*it);
}
//已经全部迁移到WaitList中,清空RegList
pEvent->RegList.clear();
return TRUE;
}
BOOL timer_insert_waitlist(TIMER* pTimer) {
EVENTOBJECT* pEvent = pTimer->pEvent;
//计算出下一次超时时间
pTimer->dwTime = pTimer->dwTimeOut + pEvent->dwCurTime;
//按时间大小排好顺序,这样激活的时候更方便提取数据
TIMERLIST::iterator it;
for (it = pEvent->WaitList.begin(); pEvent->WaitList.end() != it; it++) {
TIMER* pTimer2 = *it;
if (pTimer->dwTime > pTimer2->dwTime) {
continue;
}
else {
break;
}
}
pEvent->WaitList.insert(it, 1, pTimer);
return TRUE;
}
DWORD timer_get_min_timtout(EVENTOBJECT* pEvent) {
if (pEvent->WaitList.size() == 0) {
return -1;//无限等待
}
//因为数据是排好序的,所以第一个就是最小值
TIMER* pTimer = pEvent->WaitList.front();
DWORD dwTimeOut = 0;
//有可能早就超时了,只是还没有触发,早就超时将返回 0
if (pTimer->dwTime > pEvent->dwCurTime) {
dwTimeOut = pTimer->dwTime - pEvent->dwCurTime;
}
return dwTimeOut;
}
int timer_check_active(EVENTOBJECT* pEvent) {
//先更新一下时间
update_curtime(pEvent);
int nCount = 0;
TIMER* pTimer = NULL;
//从WiatList中找出超时的元素,然后放到ActiveList中,
while (pEvent->WaitList.size() && (pTimer = pEvent->WaitList.front())) {
if (pEvent->dwCurTime >= pTimer->dwTime) {
pEvent->WaitList.pop_front();
pEvent->ActiveList.push_back(pTimer);
nCount++;
}
else {
//因为我们WaitList已经排序,当某个元素未超时,那么它后续的
//元素也不会超时,可以退出循环了
break;
}
}
return nCount;
}
void timer_active(EVENTOBJECT* pEvent) {
TIMER* pTimer = NULL;
while (pEvent->ActiveList.size() && (pTimer = pEvent->ActiveList.front())) {
pEvent->ActiveList.pop_front();
//如果 pEvent->RegList.push_back(pTimer); 放到回调函数后面,且回调函数中
//调用了KillTimer,那么注册时将会发生崩溃!定时器的一些动作要注意先后顺序!
pEvent->RegList.push_back(pTimer);
//超时回调
pTimer->pfnFunc(pTimer, pTimer->pParam);
}
}
void update_curtime(EVENTOBJECT* pEvent) {
//GetTickCount 只能精确到 49天,应考虑用 GetTickCount64
//获取时间的函数会附带系统调用,尽量缓存起来,减少开销
//不过需要认识到这样定时器的精确度会下降
pEvent->dwCurTime = ::GetTickCount();
}
代码故意写的很简单,而且性能十分低下,因为定时器的性能问题都是一些细节问题,比较好解决,所以我尽可能把代码写的简单,希望大家更容易理解相对定时器的流程思想。
代码的重点,都在 int EventDispatch(EVENTOBJECT pEvent)* 函数的 while 循环中。
第一步是将 RegList 的元素按插入到 WaitList 中,并按下次激活的时间从小到大进行排序,这个动作完成意味着定时器异步注册成功。RegList 数据的来源有两个地方,一个是定时器激活后,调用回调函数前会先将定时器对象重新挪回RegList 。另外一个来源是 set_timer 函数。
大部分同学的第一个问题是,为什么不直接在set_timer 函数中实时注册,而是将定时器对象放到等待注册的队列中,进行异步注册。类似的问题,在 timer_check_active (检查定时器是否被激活) 的函数中也有:为什么检查到定时器激活后,不马上调用回调函数,而是先到放到激活队列中?
其实在RegList 中实时注册也是可以的,不创建 ActiveList (激活队列)对象也可以。只是加上这两个队列对象,它能适应一些更复杂场景。换而言之,如果你不需要适应复杂的场景,可以去掉其中一个或两者都去掉。
第二步是计算出最小的超时时间。其实不管对象中包含了多少个定时器,我们只需要触发超时值最小的那个对象就可以了,以它为超时数值,当超时触发后再检查总共触发了多少个定时器。在代码中我们使用的是std::list数据结构,并用 for 轮询逻辑给 WaitList对象排序。前面也说了,这里的代码只考虑可阅读性,所以没有做优化。一般是建议用二叉树数据结构 + 队列数据结构来管理注册后的超时对象的,也就是 WaitList 这个对象。
因为 WaitList 需要支持查找,插入,删除,排序,这几个操作。这恰恰是二叉树的特性,可以考虑红黑树,小根堆这些数据结构。虽然二叉树已经很高效,但插入和删除还是存在大量的旋转操作。特别是定时器这种东西,往往存在大量同时超时的定时器,所以建议再加上队列。就是相同的超时节点已经存在的情况下,加入新节点时,那么新节点不会加入二叉树的数据结构中,而是加入相同超时时间节点的队列后面。这样不管是加入相同超时时间的对象,还是删除相同超时时间的对象,都能够节省大量的旋转开销。
第三步就是根据最小超时时间,等待超时,这个没什么特别的。
第四步就检查 WaitList 对象中,有哪些对象已经已经超时了,并将其迁移到ActiveList对象中。前面也说过了,不直接触发时因为这样的代码在拓展时能适应更复杂的场景。所以我提前将 RegList 和ActiveList 这种对象的用法写出来,大家根据需要来取舍。
第五步是轮序调用激活对象的回调函数。在调用回调之前,有一个很重要的步骤:先把对象再一次加入到 RegList 中。
这样做的主要原因是,担心用户在定时器的回调函数内对自己 kill_timer。这个时候定时器对象已经无效,若再将这个无效的对象放到 RegList 中,等到正式注册时就会引发崩溃。
EventDispatch 函数的介绍就到这里了,当你能够理解这个函数的流程,其他的函数的理解基本不成问题。
我们再说几个额外的知识点。
-
定时器对象,它往往不是独立存在的,而是依附于某个对象之上,一起交互完成业务,比如网络IO,文件IO。如果是linux 的 epoll ,那定时器将会依赖 epoll_wait 函数作为超时函数。如果是windows的IOCP,
则使用 GetQueuedCompletionStatus 函数作为超时函数。 -
假设作为网络库,定时器事件的优先级都是比网络事件的优先级要低的,处理不同的事件要记得考虑优先级顺序。
-
上面的代码one loop per thread 模型的一种,思考一下。当你自己实现一个定时器后,你的 EventDispatch函数 支持在自己的回调子函数里面再调用 EventDispatch 函数吗?又或者在同一函数栈上调两次 EventDispatch 吗?参考下面的伪代码:
//场景一伪代码:(嵌套调用)
int main() {
EventDispatch()
{
Sun()//某个定时器的回调函数
{
EventDispatch();//回调函数中再调用 EventDispatch
}
}
}
//场景二伪代码:(重复调用)
int main() {
//先调用一次EventDispatch退出后,再调用一次 EventDispatch
EventDispatch();
EventDispatch();
}
//场景三伪代码:(嵌套调用) + (重复调用)
int main() {
EventDispatch()
{
Sun()//某个定时器的回调函数
{
EventDispatch();//回调函数中再调用 EventDispatch
EventDispatch();//再调一次
}
}
}
你最终设计出来的定时器,是否支持上述的调用方法呢?要知道定时器不仅需要额外的依附对象,还会间接对线程有所依赖的。所以大家不要忽视定时器带来的线程模型需求。
<完>