相对定时器实现详解

对定时器概念不是特别熟悉的同学可以先看看 定时器概述 这篇文章。
地址: https://blog.csdn.net/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 函数的介绍就到这里了,当你能够理解这个函数的流程,其他的函数的理解基本不成问题。

我们再说几个额外的知识点。

  1. 定时器对象,它往往不是独立存在的,而是依附于某个对象之上,一起交互完成业务,比如网络IO,文件IO。如果是linux 的 epoll ,那定时器将会依赖 epoll_wait 函数作为超时函数。如果是windows的IOCP,
    则使用 GetQueuedCompletionStatus 函数作为超时函数。

  2. 假设作为网络库,定时器事件的优先级都是比网络事件的优先级要低的,处理不同的事件要记得考虑优先级顺序。

  3. 上面的代码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();//再调一次
		}
	}
}

你最终设计出来的定时器,是否支持上述的调用方法呢?要知道定时器不仅需要额外的依附对象,还会间接对线程有所依赖的。所以大家不要忽视定时器带来的线程模型需求。

<完>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值