定时器概述

在日常的开发工作中,我们总是或多或少地接触到各式各样样的定时器,一般我个人喜欢将定时器划分为三类:

  1. 频率定时器
  2. 相对/绝对定时器
  3. 高精度定时器

首先说一下最简单的,频率定时器。其实就是设置一个频率的值。然后其所在的线程按照这个频率不停地进行频率触发,我们来写一下代码:

#include <windows.h>
#include <stdio.h>
typedef void (*TimerFunc)(int TimerId, void* pParam);

typedef struct __MYTIMER
{
	int nCount;//频率触发次数
	int nCurCount;//当前频率书
	int TimerId;//定时器id
	TimerFunc pfnFunc;//触发的回调函数
	void* pParam;//定时器参数

}MYTIMER;

void TimerOut(int TimerId, void* pParam)
{
	printf("TimerId = %d\n", TimerId);
}

DWORD g_dwFrequency = 1000; //频率,一秒一次
int main()
{

	MYTIMER Timer[2] = { 0 };

	Timer[0].nCount = 2;
	Timer[0].TimerId = 1;
	Timer[0].pfnFunc = TimerOut;

	Timer[1].nCount = 3;
	Timer[1].TimerId = 2;
	Timer[1].pfnFunc = TimerOut;

	while (1)
	{
		::Sleep(g_dwFrequency);//思考一下可否用其他函数代替sleep

		for (int i = 0; 2 > i; i++)
		{
			Timer[i].nCurCount++;//累计触发次数

			if (Timer[i].nCurCount == Timer[i].nCount)
			{
				Timer[i].nCurCount = 0; //达到触发条件,重置

				Timer[i].pfnFunc(Timer[i].TimerId, Timer[i].pParam);
			}
		}
	}
	return 0;
}

上面的代码很好理解,甚至有很多可以调整和优化的地方,比如将sleep改成 epoll_wait 或者 WaitForSingleObject 等。由于本章只是进行定时器概述,这里不作太详细的介绍。

通过频率定时器的代码,我们初步窥探了一个简单定时器容貌:

  1. 它拥有一个 id (不一定要用id来标识定时器,字符串也可以,甚至不要也可以)
  2. 它拥有一个触发条件,这里使用 MYTIMER::nCount 作为触发条件
  3. 它拥有一个回调函数,当判断到触发条件适合时被回调
  4. 它包含了一个时间触发器,这里使用 sleep 函数

不管何种定时器,在设计时,都大致按照上面的方式进行设计的,相对/绝对定时器是这样,高精度定时器也是这样。

再来说一下相对/绝对定时器,其实相对定时器与绝对定时器的核心逻辑设计是一样的,不同的地方在于取值问题,相对定时器一般使用系统运行时间来作为判断条件,它使用开机时间作为参考,这是一个相对的时间,所以叫相对定时器。而绝对定时器一般取系统当前时间作为判断条件。这是一个绝对的时间值,所以叫绝对定时器。
那么不同的取值方式,有什么区别呢?
区别在于,系统运行时间是不可人为变更的,而系统当前时间可以用户手动修改的。也就是意味着,若使用绝对定时器,用户手动修改了系统时间,那么程序中定时器将可能出现异常!
既然如此,绝对定时是否没必要使用呢?这不一定,如果我们写的是闹钟程序,或者定时到晚上12点,后台进行一些业务处理,这个时候用绝对时间进行判断更为合理。所以很多开源组件,都是使用绝对时间作为判断条件。但系统的API超时设计,一般都是使用相对定时器逻辑。
另外是频率定时器和相对/绝对定时器的区别,在 ::Sleep(g_dwFrequency); 中,Sleep参数的值每次都是一样的,而相对/绝对定时器的Sleep函数值是非固定的。假设有定时器对象3个,第一个是2秒,第二个是7秒,第三个是3秒。我们按超时时间进行排序:2秒,3秒,7秒。那么Sleep 函数中,第一次超时的时间是2秒,第二次超时时间是1秒(3秒 - 2秒),第三次超时时间是4秒(7秒 - 3秒)。这里的2秒,1秒,4秒超时时间,有一点是需要注意的,就是它没考虑超时函数的执行时间,更好的算法,应该是在这三个时间基础上,减去超时触发后,执行其他代码的时间消耗。
相对定时器实现,可以参考 相对定时器实现详解 这篇文章,由于定时器都是大同小异的,熟悉了相对定时器的实现,对理解其他类型的定时器也有帮助。

最后介绍一下高精度定时器。顾名思义,它的时间精度非常高,那就意味着,前面两种定时器的精度默认较低。
一般来说,高精度定时器的计时方法,可以使用频率计时,也可以使用相对/绝对计时,另外它还会为每一个定时器对象,额外创建一条休眠的线程,当超时触发时,将激活该线程,由该线程来执行对应的定时器回调函数。
为什么要这样做呢?
为了提高实时性!
要知道频率定时器和相对/绝对定时器,都是在同一条线程进行计时和触发超时回调,大家思考一下,如果上面的频率定时器,TimerOut 函数实现如下,会发生什么情况?

void TimerOut(int TimerId, void* pParam)
{
    Sleep(10 * 1000);
	printf("TimerId = %d\n", TimerId);
}

是的,后续的每个定时器,会再额外增加10秒的延时!也就是说,超时回调函数的执行时间消耗,是会直接影响到后续定时器的触发时间。目前所有操作系统当中,超时触发的时间都是延迟的,不准确的。但虽然如此,重点在于这个延迟的时间是多少,是否在我们可接受的范围之内?而一般低精度定时器,延迟都是【毫秒】级别的,当然也有可能到达【秒】的级别,假设我一个用户本来是设置了30秒后超时关闭的,那么实际情况是31秒后超时关闭,这种情况一般是可以接受的。 又或者我有一个界面渲染,本来延时100毫秒更新,实际上是120毫秒才正式执行,这种情况一般也是在接受的范围内。
再回到高精度定时器,由于它的目的是提供更高精度的实时性回调,且常规的定时器存在毫秒级别的延时。也就是说高精度定时器往往会提供毫秒级别以下的延迟触发【纳秒延迟】,这就是为什么它会预先为每个定时器对象创建一个新的线程(这个线程往往还具有更高的执行优先级),等到超时到达时,马上激活该线程,在该线程中执行超时回调函数。这就避免了下一次或下一个定时器的超时时间,会受到超时回调函数的影响。

日常的开发工作中,我们接触到的定时器,大部分都是相对/绝对定时器。比如上文Sleep函数,其实内部也是包含了定时器的实现,我们所谓自己实现的定时器,其实就是在定时器的基础上再封装一个定时器。

另外频率定时器和相对/绝对定时器,它们都是在一条线程上实现的,这条线程我们不可能光用来实现定时器,它往往还附带了其他的业务逻辑,这些循环执行的系统设计中,一般具有以下优先级处理:

  1. 先执行常规业务逻辑
  2. 再执行超时逻辑(定时器逻辑)
  3. 最后可以执行一些实时性要求不高,但十分耗时或消耗性能的逻辑,比如渲染绘制之类的逻辑。

上面仅仅是用来作为设计的参考模板,不可作为定式处理和理解!

常见的三种定时器类型,就介绍到这里了,后面会有专门的文章介绍它们的实现方案。这里我们先对常用的定时器做一下总结:

  1. 频率定时器和相对/绝对定时器,他们的下一次触发时间,会明显地受到其他逻辑代码的影响,所以超时回调函数产生时,相对于我们实际想触发的时间已经产生了延迟,所以我们要确保延迟的时间是否在可接受的时间内(一般都是可接受的)。
  2. 想要获得更高精度的超时回调,可以使用系统提供的高精度定时器。因为它都是开启新线程去执行超时回调任务的,这种定时器开销很大,如果高精度定时器对象过多,且频繁地触发有可能导致任务处理不过来,从而产生系统卡顿或死机。相比于此,频率定时器和相对/绝对定时器会受到当前线程的其他逻辑代码影响,它总是有条不紊地串行执行代码,有时候反而是一种优势。
  3. 定时器在模块的设计中,要考虑它的优先级如何处理。是与业务逻辑同级,还是低于业务逻辑,或者模块是否具备额外的优先级控制。
  4. 获取时间的函数,都是系统函数,会触发系统调用,所以写代码时,尽量不要过多调用获取时间的函数,可以考虑缓存当前时间(很多组件都是这样设计的),但这样会引起时间误差(一般实时性要求不是非常严格的,都是可以接受的)。
  5. 设计定时器的时候要注意,定时器一般都是依附于线程或某个对象之上的,当对应的线程或对象注销时,要记得顺带把定时器对象也销毁。这也意味着,当销毁线程或对象时,用户不需要再额外销毁自己创建的定时器。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值