C++ 定时器 windows平台

https://blog.csdn.net/qq_23530731/article/details/112434392


一、源代码

CSDN
github
如果有更新,会在github中同步更新,CSDN的下载不方便更新。

二、前言

C++里现有的定时器不常见。经常用的是mfc的OnTimer定时器,但这个定时器是和界面耦合的,在后台处理时会非常不方便。因此我自己封装了一个定时器。

三、实现过程

1、思路

考虑如下:
1.每个定时任务都有自己的信息,因此要定义一个结构体来记录定时任务参数。
2.考虑使用一个单独的线程来实现定时逻辑,那么用一个线程去实现一个定时任务不太合理,因此一个定时器对象应该要可以处理多个定时任务。那么就要用一个列表来管理多个定时任务。同时,用一个无符号整数(ID)来标识定时任务。
3.考虑节约资源问题,定时任务需要一个提前量,在还有几毫秒才能达到定时时间的任务,也可以执行。
4.尽量符合设计模式的原则。设计定时任务接口、定时任务基类、定时任务子类,其中定时接口负责对外表现,定时任务基类负责定时任务逻辑,定时子类负责扩展。

2、编译运行环境

在windows平台下,C++语言,ISO C++14 标准,vs2019编译器。

3、定时任务信息

用一个结构体来描述定时任务信息,如下:

	// 定时任务参数
	struct STimerTask
	{
		std::function<void(void)> m_fun; // 定时执行函数
		bool m_bLoop = false; // 是否循环执行 false只执行一次,true循环执行
		unsigned int m_nInterval = 1000; // 定时时间间隔(毫秒)
	};

4、定时任务接口

定时任务接口定义如下:

	// 定时器接口
	class CTimerInterface
	{
	public:
		CTimerInterface() {};
		virtual ~CTimerInterface() {};
		// 添加定时任务
		// nTimeId 定时Id,不能为0
		// rTask 定时任务信息
		// 返回定时任务,返回0时表示创建定时任务失败
		virtual unsigned int AddTask(unsigned int nTimeId, const STimerTask& rTask) = 0;
		// 清除所有定时任务
		virtual void Clear() = 0;
		// 移除一个定时任务
		// nTimeId 要移除的定时任务的Id
		virtual void Remove(unsigned int nTimeId) = 0;
		// 设置定时的提前量,对于给定的任务,可以提前一定时间执行,单位毫秒
		virtual void SetLeadTime(unsigned int nLeadTime)
		{
			m_nLeadTime = nLeadTime;
		}
	protected:
		unsigned int m_nLeadTime = 20; // 执行任务的提前量,对于给定的任务可以提前m_nLeadTime毫秒执行, 说明:由于C++没有属性,因此使用一个变量来定义任务提前量了。	
	};

5、定时任务基类

1)、头文件

头文件代码如下:

class CTimerBase :public CTimerInterface
	{
	public:
		// strName 定时器名称
		CTimerBase();
		virtual ~CTimerBase();

		// 添加定时任务
		// nTimeId 定时Id,不能为0
		// rTask 定时任务信息
		// 返回定时任务,返回0时表示创建定时任务失败
		virtual unsigned int AddTask(unsigned int nTimeId, const STimerTask& rTask);
		// 清除所有定时任务
		virtual void Clear();
		// 移除一个定时任务
		// nTimeId 要移除的定时任务的Id
		virtual void Remove(unsigned int nTimeId);

	protected:
		// 启动定时
		void Start();
		// 停止定时
		void Stop();

		// 定时器工作线程执行函数
		virtual void Work();
		// 遍历定时列表,执行达到时间的任务
		// 返回下一次遍历所需要的时间
		virtual unsigned __int64 TraverseExecuteTasks();
		// 执行定时任务
		virtual void ExecuteTask(const STimerTask& rTask);

		// 获取当前时间,不同的方式获取的时间,得到的定时精度不相同
		virtual unsigned __int64 GetNowTime() = 0;

	protected:
		// 内部使用的定时任务
		struct STimerTaskInter
		{
			STimerTask m_timerTask;
			unsigned __int64 m_nStartTime = 0; // 用于计算时间间隔的开始时间
		};
	protected:
		std::string m_strName; // 定时器名称

		std::mutex m_timerLock; // 定时任务锁
		std::mutex m_startStopWorkThreakLock; // 启停工作线程锁
		std::atomic<bool> m_bRun = false; // 工作线程运行标志
		std::thread* m_pWorkThreak = nullptr; // 工作线程
		std::condition_variable_any m_workCondition; // 定时用条件变量,用其超时特性来定时,在定时的过程中也能随时唤醒
		std::map<unsigned int,STimerTaskInter> m_mapTimerTask; // 定时任务集合
	};
};

有很多备注,就简要说明几点:
1.内部逻辑中,会用到开始定时的时间这个额外的参数,但这个参数是内部维护的不展示给调用者,因此额外定义了一个STimerTaskInter结构体。
2.本来是想将定时器的运行线程设置上名称的,这样可以方便调试。但技术上遇到一些问题,无法完美,就没实现了。
3.用一个map来保存多个任务,其中key为定时ID,值为定时任务参数信息。

2)、实现

定时任务基类的实现部分,AddTask、Clear、Remove是对map的维护,不说了。
Start和Stop是对定时任务线程的启停控制,当没有定时任务时,线程不运行,当有任务后,要保证线程运行。同时Stop也要等待工作线程结束。略过。
Work函数是处理定时任务逻辑线程的实现,其代码如下:

void CTimerBase::Work()
	{
		unsigned __int64 nMinWaitTime = 0; // 所有任务中最小的等待时间,用于计算下一次执行需要多久
		std::unique_lock<std::mutex> lks(m_timerLock);
		while (m_bRun)
		{
			m_workCondition.wait_for(m_timerLock, std::chrono::milliseconds(nMinWaitTime));
			if (!m_bRun)
			{
				return;
			}
			nMinWaitTime = TraverseExecuteTasks();
			if (m_mapTimerTask.empty()) // 如果没有任务了,就退出
			{
				break;
			}
		}
	}

TraverseExecuteTasks函数的代码如下:

	unsigned __int64 CTimerBase::TraverseExecuteTasks()
	{
		unsigned __int64 nMinWaitTime = 0xffffffffffffffff; // 所有任务中最小的等待时间,用于计算下一次执行需要多久
		__int64 hadWaitTime = 0; // 一个任务已经等待了的时间
		__int64 nNeedWaitTime = 0; // 一个任务还需要等待的时间

		unsigned __int64 nNowTime = GetNowTime();
		for (std::map<unsigned int, STimerTaskInter>::iterator it = m_mapTimerTask.begin(); it != m_mapTimerTask.end();)
		{
			STimerTaskInter& rTimerTaskInter = it->second;
			hadWaitTime = nNowTime - rTimerTaskInter.m_nStartTime; // 已经等待了的时间
			if (hadWaitTime < 0) // 不能小于0
			{
				hadWaitTime = 0;
			}

			if ((hadWaitTime + m_nLeadTime) >= rTimerTaskInter.m_timerTask.m_nInterval) // 时间间隔大于指定间隔时执行 可以提前m_nLeadTime毫秒执行
			{
				ExecuteTask(rTimerTaskInter.m_timerTask);
				rTimerTaskInter.m_nStartTime = nNowTime;
				if (!rTimerTaskInter.m_timerTask.m_bLoop) // 如果定时器只执行一次,则移除
				{
					it = m_mapTimerTask.erase(it);
					continue;
				}

				nNeedWaitTime = rTimerTaskInter.m_timerTask.m_nInterval; // 一个任务还需要等待的时间
			}
			else
			{
				nNeedWaitTime = rTimerTaskInter.m_timerTask.m_nInterval - hadWaitTime; // 一个任务还需要等待的时间
			}
			assert(nNeedWaitTime > 0); // 按照逻辑,nNeedWaitTime必然大于0

			// 找到最小的需要等待的时间
			if ((unsigned __int64)nNeedWaitTime < nMinWaitTime)
			{
				nMinWaitTime = nNeedWaitTime;
			}

			++it;
		}

		assert(nMinWaitTime > 0); // nMinWaitTime必然大于0
		return nMinWaitTime;
	}

work函数中有一个循环和TraverseExecuteTasks配合工作。每次先判断是否有定时任务达到时间了,达到就执行。然后,还要计算出最近的定时任务到达时间。最后再睡眠计算出的时间。以便程序睡眠,不用每一毫秒都去遍历。

6、定时任务子类

在windows平台下,我找到获取当前系统时间的方法有两个:
QueryPerformanceCounter(…)
GetTickCount64(…)
其中,QueryPerformanceCounter精度高。GetTickCount64精度低,微软文档中说要隔十几毫秒才刷新一次值。在不同的条件下,需求不同的定时间隔。因此,在定时任务基类中,将获取系统时间功能定义为了纯虚函数,在子类中去实现不同的精度的定时。
对两种精度,实现了两个子类类,CTickTimer和CHighPrecisionTimer。在使用时可以适当选用。这两个子类仅仅需要实现GetNowTime函数,逻辑简单,就略过了,请自行查看代码。

7、调用

在Main.cpp中,展示了定时任务的使用方法,其代码如下:

#include <iostream>
#include <sstream>
#include <functional>
#include "Timer/Head.h"

// 定时测试函数
void TimerTest(int n)
{
	std::cout << "TimerTest, n = " << n << std::endl;
}


// 定时测试类
class CTimerTest
{
public:
	// 成员变量作为定时执行的测试函数
	void TimerTest(int n)
	{
		std::cout << "CTimerTest::TimerTest, n = " << n << std::endl;
	}
};

int main()
{
	jaf::CHighPrecisionTimer timer; // 定义一个定时器

	int n = 1; // 测试用参数

	jaf::STimerTask task; // 定时任务
	task.m_nInterval = 5000; // 定时5秒
	task.m_bLoop = false; // 不循环定时
	task.m_fun = std::bind(&TimerTest, n); // 用全局函数作为定时执行函数
	timer.AddTask(1,task); // 添加第一个定时任务

	task.m_nInterval = 1000; // 定时1秒
	task.m_bLoop = true; // 循环定时
	CTimerTest timerTest;
	task.m_fun = std::bind(&CTimerTest::TimerTest, &timerTest, n); // 用类的成员函数作为定时执行函数
	timer.AddTask(2,task); // 添加第2个定时任务


	std::cout << "输入任意字符停止定时" << std::endl;
	getchar();
	timer.Remove(2);
	//timer.Clear();

	system("pause");
	return 0;
}

注释很多,就略过了,请自行查阅。

最后

如果有什么问题、建议、读后感,可以留言讨论哟,我在有空的时候会回复。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值