运用Windows定时器队列实现高精度定时
0 引言
在软件开发过程中,定时器是一种常用的设计元素。Windows 平台上的定时器编程有多种实现方式,比较常用的有:SetTimer()实现的定时器、timeSetEvent()函数实现的多媒体定时器、可等待定时器(Waitable timer)、相关的Windows API函数结合循环采用轮询机制实现的定时功能,以及定时器队列定时器(Timer-queue timer)等。
SetTimer()函数可以为Windows 程序分配一个定时器,该定时器利用WM_TIMER 消息映射来进行简单的时间控制,定时精度约为55ms.由于Windows系统采用多线程的抢占式多任务工作方式,而WM_TIMER消息优先级较低,因此这种定时方法适用于对定时精度要求不高的情况。Delphi、VB等快速原型化开发工具中自带的定时器控件,基本上都是采用该方法实现的。除了定时精度低以外,VB的定时器控件有更大的缺陷,其定时间隔不能超过65535ms,更限制了这样的定时器控件的应用。
与SetTimer()函数实现的定时器相比,timeSetEvent()实现的多媒体定时器,可以实现多分辨率的定时,分辨率越高,定时精度越高,但系统所需的开销越重。该函数可以实现只进行一次性的定时事件出发,也可以进行常规的周期性定时,因此该方法的应用比较灵活,但出于种种原因,微软在MSDN中明确指出,该函数已被摒弃,在应用程序开发过程中不应再使用该方法进行定时。
Windows 中很多与时间相关的API 函数可以与循环结合,采用轮询机制实现定时功能。典型的API函数有GetTickCount()、timeGetTime()、QueryPerformanceFrequency ()和QueryPerformanceCounter()。GetTickCount()和timeGetTime()都是获取自系统启动以来流逝的秒数,结合循环,获取两次调用之间的差值即可实现定时。QueryPerformanceFrequency()和QueryPerformanceCounter()配合使用,QueryPerformanceFrequency()函数获取机器内部定时器的时钟频率,在需要定时的事件发生之前和发生之后分别调用QueryPerformanceCounter()函数,利用两次获得的计数之差,结合已获取的时钟频率,即可计算出两件事件之间经历的精确的时间长度,严格地来说,在Windows平台上,采用这两个函数实现的定时最为精确的,但由于需要结合循环进行轮询,因此在定时过程中需要不停地消耗CPU资源,是一种"忙等";机制。
可等待定时器是Windows中的一种内核对象,因此该定时器的使用模式与其它内核对象的使用方式基本是一致的。CreateWaitableTimer()函数用于创建可等待定时器对象,SetWait ableTimer()函数设置等待的时长,WaitForSingleObject()函数等待该定时器对象有信号,如定时器对象有信号,则表明指定时长的时间已经流逝。作为一种内核对象,可等待定时器可以跨线程、进程使用,但在实际应用中,一般多用于线程同步或进程同步,而用于周期性定时的用法,并不多见。
定时器队列定时器是微软在Windows2000及后续的Windows 操作系统中推荐使用的定时器,它支持多种工作模式,创建定时器队列定时器时,需指定一个回调函数,当定时时间到达时,自动调用回调函数
实现周期性定时。顾名思义,这种定时器采用队列机制进行管理,是一种轻型对象,而且系统对回调函数采用线程池机制进行管理、调度,因此系统开销较小。
从以上分析可知,使用定时器队列实现高精度定时具有明显的优越性,因此在Windows2000及后续的Windows操作系统中用来替代timeSetEvent()多媒体定时器。为了方便使用,本文使用Visual Basic6.0,利用定时器队列开发了一个ActiveX定时器控件,该控件拥有定时器队列定时器的一切优势,可以实现定时间隔最长为2147483647ms的高精度定时。
1 定时器队列实现定时功能的基本原理
如前所述,定时器队列定时器可以支持多种工作模式,其实现绕过了Windows消息队列机制,定时精度高。
Windows 提供了一系列API 函数用于创建、管理定时器队列及其中的定时器。使用定时器队列定时器,首先需要调用CreateTimerQueue()函数创建一个定时器队列,该函数无参数,成功调用时返回定时器队列的句柄,否则返回空,原型为:
2 将定时器设计成ActiveX 控件
各种支持ActiveX控件的软件都有统一的接口,ActiveX控件在一种软件下开发而在其他软件中可以使用
的控件,能极大增强软件的功能和提高代码复用的效率,本文运用上述定时器队列定时器的API函数,在简单高效的Visual Basic 6.0平台上,利用ActiveX技术将其开发成ActiveX控件,使其为工业控制和数据采集中的定时功能提供方便的服务。
首先创建名为lActiveX工程,将其设置为运行时不可见,即在属性窗口中将属性InvisibleAtRuntime 设置为True,该定时器所用的几个API 函数位于kernel32.dll 中,先予以声明,在代码窗口中为ActiveX控件定义三个属性和一个事件,其中布尔型的变量m_Enabled是Enabled属性的值,表示定时控件是否可用,long型的m_Interval是定时器的时长以ms为单位, 是一个长整数值, 在本次实验中为8 小时即28800000ms, m_FirstInterval 表示经过多长时间开始定时,Timer 是该控件的一个重要的事件,在定时时间到时由回调函数触发运行。
当Enabled属性改变或者Interval属性值被修改时,并且控件处于运行状态,则要删除旧的定时器,当用户将Enabled属性False改变为True时,创建新的定时器,Enabled 属性由True变为False时,删除定时器。关键代码如下:
在上述代码中要用AddressOf 获取控件新的窗口过程地址和回调函数的入口地址,因此不能把窗口过程和回调函数写在ActiveX控件代码段内,需要在一个模块文件中编写。下面是新的窗口过程函数WindowProc,作用是处理控件窗口上的所有消息,其中最主要的是处理定时器的回调函数发来的消息,
代码中用GetProp(hwnd, "QTimer";)读取QTimer属性的值,获取定时器的句柄,启动FireTime事件,窗口的其他消息仍然由原窗口过程处理,用GetProp(hwnd, " OldWinProc ";)读取OldWinProc 属性的值,获取原窗口过程入口地址,进行其他消息的处理。
3 控件的测试
将上述代码对应的ActiveX控件,编译后,注册到需定时的工程控制程序中进行测试, 将Interval 值被设置为28800000,被用来进行每隔8 小时循环定时,程序连续运行24天,累计定时误差不超过30秒钟。
waitforsingleobject函数4 总结
本文通过分析几种软件定时器的性能,经过实验,摒弃了其他几种方法,并且了解了他们的适用场合,最终选用定时器队列定时器,开发了ActiveX控件进行定时,能够进行超长高精度的定时功能,方便了用户在各种平台下的使用,目前在Windows XP和windows 7下注册测试并使用,效果良好。
参考文献
卓红艳,赵平。基于VC_的实时数据采集系统中定时器的使用与比较. 现代电子技术,2007年第18期。
【2】刘春风,田延岭。Windows 操作系统下的软件定时器的设计与应用,机电一体化,2004年第5期。
毕业,史忠科。Windows2000 下高精度定时器设计与实现. 工业仪表与自动化装置,2007年第一期。
李四保,姚晓先。Win32s 下内核定时器的使用. 微计算机信息,2003 年第2 期。
李海。Visual Basic 编程晋级:ActiveX 控件. 北京航空航天大学出版社,2000年1月。