天天看點

WM_TIMER消息線上程被阻塞時的系統處理

    我的腦海中忽然對這樣一個問題有一些模糊,也就是當一個安裝了定時器的線程被阻塞期間,定時器消息如何被送往消息隊列?線上程從阻塞狀态恢複以後,消息隊列的狀态是怎麼樣的?是否裡面聚集多個WM_TIMER消息?還是阻塞期間沒有收到WM_TIMER消息,還是在阻塞期間多個應該送達的WM_TIMER被合并成了一個?(類似WM_PAINT消息那樣)。

    是以我做了一個小實驗來驗證這個問題,結果我發現結論是最後一種情況,即可能系統在被喚起應該像某個線程的消息隊列投遞WM_TIMER消息時,它如果發現消息隊列中已經有相同的WM_TIMER消息(ID号相同),則可能放棄投遞,否則才會投遞。這樣就符合我們觀察到的結果,即阻塞期間應該産生的多個定時器消息看起來仿佛被合并成了一個。

    這個試驗是這樣的,我給UI線程安裝一個5秒鐘間隔的定時器(收到定時器消息後在視窗進行輸出),然後發起另一個線程,阻塞 UI 線程21秒的時間。然後觀察UI線程的輸出,效果如下:

    

WM_TIMER消息線上程被阻塞時的系統處理

    我們繪制一個更直覺的的圖形來解釋上面的輸出,如下:

WM_TIMER消息線上程被阻塞時的系統處理

    圖中,紅色的箭頭是UI線程實際收到的定時器消息,藍色箭頭是阻塞期間應該産生消息的時間。可見,在阻塞期間應該産生 4 個 WM_TIMER 消息,但實際上線上程從阻塞狀态恢複後,立即處理了僅僅一條。此後消息仍然按照既定間隔定期發送。

    上文中用于測試的代碼如下:

WM_TIMER消息線上程被阻塞時的系統處理
WM_TIMER消息線上程被阻塞時的系統處理

CODE_WM_TIMER_TEST

#include "stdafx.h"

#include <string>

#include "resource.h"

using namespace std;

HINSTANCE hInst;

string m_msg;

int blockTime;

LRESULT CALLBACK MyDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);

//測試線程

DWORD WINAPI TestThread(void* pArg);

int APIENTRY WinMain(HINSTANCE hInstance,

                     HINSTANCE hPrevInstance,

                     LPSTR     lpCmdLine,

                     int       nCmdShow)

{

     // TODO: Place code here.

    hInst = hInstance;

    DialogBoxParam(hInst, MAKEINTRESOURCE(IDD_DIALOG1), NULL, (DLGPROC)MyDlgProc, 0);

    return 0;

}

// 對話框

LRESULT CALLBACK MyDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)

    static int b_timerOn;

    static int timerNum;

    switch(message)

    {

    case WM_INITDIALOG:

        b_timerOn = 0;

        timerNum = 0;

        break;

    case WM_COMMAND:

        {

            WORD ctl = LOWORD(wParam);

            switch(ctl)

            {

            case IDOK:

            case IDCANCEL:

                EndDialog(hDlg, ctl);

                return TRUE;

            case IDC_BT_SETTIMER:

                if(b_timerOn)

                {

                    SetDlgItemText(hDlg, ctl, "開始計時器");

                    KillTimer(hDlg, 1);

                }

                else

                    SetDlgItemText(hDlg, ctl, "停止計時器");

                    SetTimer(hDlg, 1, 5000, NULL);

                b_timerOn ^= 1; //取反(在0,1之間切換)

            case IDC_BT_STARTTHREAD:

                    DWORD threadId;

                    SYSTEMTIME st;

                    blockTime = 21000;

                    char line[128];

                    HANDLE hThread = CreateThread(NULL, 0,

                        TestThread, 

                        (LPVOID)&blockTime,

                        0, //立即執行

                        &threadId

                        );

                    //sprintf(line, "thread: %ld start...\r\n", threadId);

                    //m_msg += line;

                    //SetDlgItemText(hDlg, IDC_MSG, m_msg.c_str());

                    //阻塞主線程

                    WaitForSingleObject(hThread, INFINITE);

                    CloseHandle(hThread);

                    GetLocalTime(&st);

                    sprintf(line, "%02d:%02d: thread: %ld exit...\r\n", st.wMinute, st.wSecond, threadId);

                    m_msg += line;

                    SetDlgItemText(hDlg, IDC_MSG, m_msg.c_str());

            }

        }

    case WM_TIMER:

            SYSTEMTIME st;

            char text[96];

            GetLocalTime(&st);

            sprintf(text, "%02d:%02d WM_TIMER_%04ld\r\n", st.wMinute, st.wSecond, timerNum);

            timerNum++;

            m_msg = m_msg + text;

            SetDlgItemText(hDlg, IDC_MSG, m_msg.c_str());

        return TRUE;

    case WM_DESTROY:

        KillTimer(hDlg, 1);

    }

    return FALSE;

DWORD WINAPI TestThread(void* pArg)

    int* pBlockTime = (int*)pArg;

    Sleep(*pBlockTime);

    結論:根據上面的觀察,可以認為,定時器消息和 繪制消息類似,在程序被阻塞期間,多個定時器消息可能被系統透明的合并成了一條消息。在從阻塞狀态恢複後,定時器扔按照原有間隔繼續發送。

    請注意,不要認為所有WM_TIMER會嚴格按照響應時間産生(即不要認為一定能産生相應的數量),不要認為每一條一定會在某個時刻得到處理(即其被處理的時間也取決于線程的運作狀态,例如被阻塞所拖延)。可以認為在阻塞期間的所有定時器消息僅會線上程停止阻塞後處理一次。