在任何语言任何操作系统下的开发中,定时器都是一个必不可少的功能,大部分的操作系统和语言都有内置的定时器接口可供调用。在windows API中有一组定时器相关函数,包括CreateTimerQueue、DeleteTimerQueue、CreateTimerQueueTimer、DeleteTimerQueueTimer,可以很方便的实现定时器相关功能。
CreateTimerQueue,创建定时器队列;
DeleteTimerQueue,销毁定时器队列;
DeleteTimerQueueEx,销毁定时器队列,与上一个函数的区别在于,这个函数的第二个参数可以指定是否等待当前队列中的回调函数执行完再返回;
CreateTimerQueueTimer,创建定时器;
DeleteTimerQueueTimer,销毁定时器;
具体函数定义可参见MSDN。
最近在使用这组定时器函数的过程中遇过一个问题,某个线程在调用DeleteTimerQueueTimer函数时block住了,无法正常返回。仔细阅读MSDN上关于这个函数的说明:
BOOL WINAPI DeleteTimerQueueTimer( _In_opt_ HANDLE TimerQueue, _In_ HANDLE Timer, _In_opt_ HANDLE CompletionEvent);Parameters
TimerQueue [in, optional]A handle to the timer queue. This handle is returned by the CreateTimerQueue function.
If the timer was created using the default timer queue, this parameter should be NULL.
Timer [in]A handle to the timer-queue timer. This handle is returned by the CreateTimerQueueTimer function.
CompletionEvent [in, optional]A handle to the event object to be signaled when the system has canceled the timer and all callback functions have completed. This parameter can be NULL.
If this parameter is INVALID_HANDLE_VALUE, the function waits for any running timer callback functions to complete before returning.
If this parameter is NULL, the function marks the timer for deletion and returns immediately. If the timer has already expired, the timer callback function will run to completion. However, there is no notification sent when the timer callback function has completed. Most callers should not use this option, and should wait for running timer callback functions to complete so they can perform any needed cleanup.
在我的代码中最后一个参数赋值为INVALID_HANDLE_VALUE而非NULL,也就说会等待任何该定时器的回调函数执行完再返回。从程序的执行log来看,在跑到DeleteTimerQueueTimer这个函数后,定时器的回调函数仍然被调用了很多次,DeleteTimerQueueTimer并没有在某个回调函数执行完后就正常返回。
由于不知道windows这几个定时器函数具体如何实现的(如果有哪位高人了解,万望知悉),只能根据现有现象猜测个大概。
假设某个Timer的执行间隔是1s,那么每隔1s,系统会将它的回调函数放到Timer线程的执行队列中(CreateTimerQueueTimer的最后一个参数可以指定是否在Timer当前线程执行回调函数),如果当前Timer队列为空,即前面的回调函数都已执行完毕,那么新加入的回调函数就能够立即得到执行,这样这个Timer就处于一个健康状态,每个回调函数都能在指定时间执行,并在指定时间间隔内返回。
假设一种异常情况,某个Timer的执行间隔为1s,但是它的回调函数会执行2s,那么除了第一次执行,后面的每次调用,都会比原定时间更晚,第二次调用在2s后,晚了1s,第三次调用在4s后,晚了2s,依次类推。如果DeleteTimerQueueTimer这个函数的实现也是把销毁定时器这个动作放到了Timer线程队列中,那么Timer的回调函数执行了n次,DeleteTimerQueueTimer的执行就会被相应的推迟n-1s,在n足够大时,DeleteTimerQueueTimer就会表现为block住,无法返回,不过最终还是能返回的,并不是死循环。
为了印证这种想法,我写了个很简单的程序:
VOID CALLBACK TimerCallback( PVOID lpParameter, BOOLEAN TimerOrWaitFired ){ Sleep(100 * 1000); }int main(){ HANDLE timer_queue = CreateTimerQueue(); HANDLE timer; CreateTimerQueueTimer(&timer, timer_queue, TimerCallback, NULL, 0, 10, WT_EXECUTEINTIMERTHREAD); Sleep(100*1000); DeleteTimerQueueEx(timer_queue, INVALID_HANDLE_VALUE); return 0;}
代码中定时器执行间隔10ms,回调函数会执行100s,然后在开始运行100s后销毁定时器。然后F5运行,然后......从中午等到半夜12点还是没返回,就卡在了DeleteTimerQueueEx,这里我很想放个哭瞎的表情,然而好像并不能放动图。按照我所猜测的定时器实现原理,我算了一下,当前这个函数要等27天半左右才能执行完。。。真是等到天荒地老。
之后稍微了改了下执行参数,定时器执行100ms,运行10s后调用DeleteTimerQueueEx,果然等待时间大大缩短,1分钟左右就返回了。
从上面这个实验来看,windows这一组定时器函数的实现原理应该就和我想的差不多。那么了解了原理之后,改问题就好改多了。要避免销毁定时器时block住,主要有两个方面需要考虑:
1. 避免回调函数的执行时间超过调用间隔;
2. 避免将所有定时器创建在一个线程中。CreateTimerQueue时,系统会创建一个Timer线程,后面调用CreateTimerQueueTimer时,最后一个参数指定为WT_EXECUTEINTIMERTHREAD时,系统会将该新创建的Timer的回调函数放到默认的Timer线程队列中。也可以将最后一个参数指定为WT_EXECUTELONGFUNCTION,由系统判断是否为该定时器的创建新线程,如果定时器的回调函数会执行比较久,那么最好使用这个参数,否则会影响其他定时器的正常运行。
以上就是本人最近使用windows定时器的一些总结,欢迎各路大拿拍砖(隐隐的还是感觉很多有问题的地方)。
新闻热点
疑难解答