我的脑海中忽然对这样一个问题有一些模糊,也就是当一个安装了定时器的线程被阻塞期间,定时器消息如何被送往消息队列?在线程从阻塞状态恢复以后,消息队列的状态是怎么样的?是否里面聚集多个WM_TIMER消息?还是阻塞期间没有收到WM_TIMER消息,还是在阻塞期间多个应该送达的WM_TIMER被合并成了一个?(类似WM_PAINT消息那样)。
所以我做了一个小实验来验证这个问题,结果我发现结论是最后一种情况,即可能系统在被唤起应该像某个线程的消息队列投递WM_TIMER消息时,它如果发现消息队列中已经有相同的WM_TIMER消息(ID号相同),则可能放弃投递,否则才会投递。这样就符合我们观察到的结果,即阻塞期间应该产生的多个定时器消息看起来仿佛被合并成了一个。
这个试验是这样的,我给UI线程安装一个5秒钟间隔的定时器(收到定时器消息后在窗口进行输出),然后发起另一个线程,阻塞 UI 线程21秒的时间。然后观察UI线程的输出,效果如下:
我们绘制一个更直观的的图形来解释上面的输出,如下:
图中,红色的箭头是UI线程实际收到的定时器消息,蓝色箭头是阻塞期间应该产生消息的时间。可见,在阻塞期间应该产生 4 个 WM_TIMER 消息,但实际上在线程从阻塞状态恢复后,立即处理了仅仅一条。此后消息仍然按照既定间隔定期发送。
上文中用于测试的代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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之间切换)
return TRUE;
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());
}
return TRUE;
}
}
break;
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 TRUE;
}
return FALSE;
}
//测试线程
DWORD WINAPI TestThread(void* pArg)
{
int* pBlockTime = (int*)pArg;
Sleep(*pBlockTime);
return 0;
}
结论:根据上面的观察,可以认为,定时器消息和 绘制消息类似,在进程被阻塞期间,多个定时器消息可能被系统透明的合并成了一条消息。在从阻塞状态恢复后,定时器扔按照原有间隔继续发送。
请注意,不要认为所有WM_TIMER会严格按照响应时间产生(即不要认为一定能产生相应的数量),不要认为每一条一定会在某个时刻得到处理(即其被处理的时间也取决于线程的运行状态,例如被阻塞所拖延)。可以认为在阻塞期间的所有定时器消息仅会在线程停止阻塞后处理一次。