1. 程式人生 > >Windows中定時器Timer使用中的注意事項

Windows中定時器Timer使用中的注意事項

在任何語言任何作業系統下的開發中,定時器都是一個必不可少的功能,大部分的作業系統和語言都有內建的定時器介面可供呼叫。在windows API中有一組定時器相關函式,包括CreateTimerQueue、DeleteTimerQueue、CreateTimerQueueTimer、DeleteTimerQueueTimer,可以很方便的實現定時器相關功能。

CreateTimerQueue,建立定時器佇列;

DeleteTimerQueue,銷燬定時器佇列;

DeleteTimerQueueEx,銷燬定時器佇列,與上一個函式的區別在於,這個函式的第二個引數可以指定是否等待當前佇列中的回撥函式執行完再返回;

CreateTimerQueueTimer,建立定時器;

DeleteTimerQueueTimer,銷燬定時器;

具體函式定義可參見MSDN。

最近在使用這組定時器函式的過程中遇過一個問題,某個執行緒在呼叫DeleteTimerQueueTimer函式時block住了,無法正常返回。仔細閱讀MSDN上關於這個函式的說明:

Syntax

C++
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定時器的一些總結,歡迎各路大拿拍磚(隱隱的還是感覺很多有問題的地方)。