在Windows界面开发中,启动定时器的最常用方法是使用SetTimer这个API。通过这个API启动的定时器会持续不断地往窗口消息队列中投递WM_TIMER消息,直到调用了KillTimer来停止。一个有趣的问题是,假如定时器的消息程序处理不过来,即处理WM_TIMER的时间比定时器的间隔时间长,会发生什么事情呢?消息队列中是否会堆积越来越多的WM_TIMER消息?官方文档中并没有指出这个问题,只能通过实践来找出答案。
定时器有多种使用场景,下面针对每种场景分别进行试验。
一个窗口一个定时器
首先是最简单的使用场景,在一个窗口中启动一个定时器。使用下面的代码生成一个Windows应用程序(为了便于阅读,省略了注册窗口类和创建窗口的代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include <Windows.h> #include <sstream>
LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
DWORD g_begin_tick_count = 0; DWORD g_counter = 0; const int kTimerId = 1;
int WINAPI WinMain(HINSTANCE, HINSTANCE, char*, int) {
RegisterClassEx(...); HWND window_handle = CreateWindowEx(...);
g_begin_tick_count = GetTickCount(); SetTimer(window_handle, kTimerId, 1000, nullptr);
MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
return 0; }
LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) { case WM_TIMER: {
std::wstringstream stream; stream << L"Process WM_TIMER. " << L"TimerId: " << wParam << ". " << L"Counter: " << ++g_counter << ", " << L"Time: " << GetTickCount() - g_begin_tick_count << '.' << std::endl;
std::wstring string = stream.str(); OutputDebugString(string.c_str());
if (g_counter < 5) { Sleep(5000); } return 0; }
default: return CallWindowProc(DefWindowProc, hwnd, message, wParam, lParam); } }
|
上面的代码创建了一个间隔时间为1秒的定时器,在处理WM_TIMER的时候,输出定时器ID,消息个数以及当前时间。g_counter
全局变量记录处理过的WM_TIMER消息的数量;时间的计算使用GetTickCount函数,单位是毫秒。在处理前面4个WM_TIMER的时候,使用Sleep函数使程序挂起5秒,模拟处理时间过长的情景。
程序总的挂起时间是4*5=20
秒,在这段时间内,定时器理应触发20次,即投递20个WM_TIMER消息,但是程序只能处理其中的3个(第一个不算)。假如WM_TIMER消息会堆积,那么从第5个开始,由于不再挂起程序,这些堆积的消息可以一口气处理完。观察程序是否会在短时间内连续输出,即可验证这个假设。
启动程序,静置一段时间之后,输出如下:
1 2 3 4 5 6 7 8 9 10
| Process WM_TIMER. TimerId: 1. Counter: 1, Time: 1014. Process WM_TIMER. TimerId: 1. Counter: 2, Time: 6022. Process WM_TIMER. TimerId: 1. Counter: 3, Time: 11029. Process WM_TIMER. TimerId: 1. Counter: 4, Time: 16037. Process WM_TIMER. TimerId: 1. Counter: 5, Time: 21045. Process WM_TIMER. TimerId: 1. Counter: 6, Time: 21294. Process WM_TIMER. TimerId: 1. Counter: 7, Time: 22308. Process WM_TIMER. TimerId: 1. Counter: 8, Time: 23322. Process WM_TIMER. TimerId: 1. Counter: 9, Time: 24336. Process WM_TIMER. TimerId: 1. Counter: 10, Time: 25350.
|
可以看到,在处理了第5个WM_TIMER消息之后,紧接着就处理了第6个,接下来每隔1秒处理一个,并没有一口气处理了一批。也就是说,WM_TIMER消息并不会堆积。
一个窗口多个定时器
如果一个窗口中多有个定时器,其中某个定时器处理不过来,对其它的定时器有什么影响呢?继续进行试验,把上面的代码稍作修改,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| #include <Windows.h> #include <sstream>
LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
DWORD g_begin_tick_count = 0; DWORD g_counter1 = 0; DWORD g_counter2 = 0; const int kTimerId1 = 1; const int kTimerId2 = 2;
int WINAPI WinMain(HINSTANCE, HINSTANCE, char*, int) {
RegisterClassEx(...); HWND window_handle = CreateWindowEx(...);
g_begin_tick_count = GetTickCount(); SetTimer(window_handle, kTimerId1, 1000, nullptr); SetTimer(window_handle, kTimerId2, 1000, nullptr);
MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
return 0; }
LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) { case WM_TIMER: {
std::wstringstream stream; stream << L"Process WM_TIMER. " << L"TimerId: " << wParam << ". " << L"Counter: " << (wParam == kTimerId1 ? ++g_counter1 : ++g_counter2) << ", " << L"Time: " << GetTickCount() - g_begin_tick_count << '.' << std::endl;
std::wstring string = stream.str(); OutputDebugString(string.c_str());
if ((wParam == kTimerId1) && (g_counter1 < 5)) { Sleep(5000); } return 0; }
default: return CallWindowProc(DefWindowProc, hwnd, message, wParam, lParam); } }
|
上面的代码创建了两个间隔都是1秒的定时器,分别用g_counter1
和g_counter2
两个全局变量来记录它们处理过的WM_TIMER消息的数量。同样地,在处理第一个定时器的前4个WM_TIMER消息时,调用Sleep函数挂起程序5秒。第二个定时器的WM_TIMER消息不做特殊处理。
启动程序,静置一段时间之后,输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Process WM_TIMER. TimerId: 2. Counter: 1, Time: 1014. Process WM_TIMER. TimerId: 1. Counter: 1, Time: 1014. Process WM_TIMER. TimerId: 2. Counter: 2, Time: 6022. Process WM_TIMER. TimerId: 1. Counter: 2, Time: 6022. Process WM_TIMER. TimerId: 2. Counter: 3, Time: 11030. Process WM_TIMER. TimerId: 1. Counter: 3, Time: 11030. Process WM_TIMER. TimerId: 2. Counter: 4, Time: 16037. Process WM_TIMER. TimerId: 1. Counter: 4, Time: 16037. Process WM_TIMER. TimerId: 2. Counter: 5, Time: 21045. Process WM_TIMER. TimerId: 1. Counter: 5, Time: 21045. Process WM_TIMER. TimerId: 2. Counter: 6, Time: 21295. Process WM_TIMER. TimerId: 1. Counter: 6, Time: 21295. Process WM_TIMER. TimerId: 2. Counter: 7, Time: 22309. Process WM_TIMER. TimerId: 1. Counter: 7, Time: 22309. Process WM_TIMER. TimerId: 2. Counter: 8, Time: 23323. Process WM_TIMER. TimerId: 1. Counter: 8, Time: 23323. Process WM_TIMER. TimerId: 2. Counter: 9, Time: 24337. Process WM_TIMER. TimerId: 1. Counter: 9, Time: 24337. Process WM_TIMER. TimerId: 2. Counter: 10, Time: 25351. Process WM_TIMER. TimerId: 1. Counter: 10, Time: 25351.
|
两个定时器的行为基本一致,而且跟上一个场景一样,在连续处理了第5个和第6个WM_TIMER消息之后,还是每隔1秒处理一个。可见,第一个定时器处理不过来,会影响到第二个定时器,但是它们的WM_TIMER消息都不会堆积。
多个窗口多个定时器
最后再看看在不同窗口中启动多个定时器的场景。修改上一个场景的代码,在另一个窗口中创建第二个定时器,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int WINAPI WinMain(HINSTANCE, HINSTANCE, char*, int) {
RegisterClassEx(...); HWND window_handle1 = CreateWindowEx(...); HWND window_handle2 = CreateWindowEx(...);
g_begin_tick_count = GetTickCount(); SetTimer(window_handle1, kTimerId1, 1000, nullptr); SetTimer(window_handle2, kTimerId2, 1000, nullptr);
MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
return 0; }
|
其余的代码保持不变。
启动程序,静置一段时间之后,输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Process WM_TIMER. TimerId: 2. Counter: 1, Time: 1014. Process WM_TIMER. TimerId: 1. Counter: 1, Time: 1014. Process WM_TIMER. TimerId: 2. Counter: 2, Time: 6022. Process WM_TIMER. TimerId: 1. Counter: 2, Time: 6022. Process WM_TIMER. TimerId: 2. Counter: 3, Time: 11030. Process WM_TIMER. TimerId: 1. Counter: 3, Time: 11030. Process WM_TIMER. TimerId: 2. Counter: 4, Time: 16037. Process WM_TIMER. TimerId: 1. Counter: 4, Time: 16037. Process WM_TIMER. TimerId: 2. Counter: 5, Time: 21045. Process WM_TIMER. TimerId: 1. Counter: 5, Time: 21045. Process WM_TIMER. TimerId: 2. Counter: 6, Time: 21295. Process WM_TIMER. TimerId: 1. Counter: 6, Time: 21295. Process WM_TIMER. TimerId: 2. Counter: 7, Time: 22309. Process WM_TIMER. TimerId: 1. Counter: 7, Time: 22309. Process WM_TIMER. TimerId: 2. Counter: 8, Time: 23323. Process WM_TIMER. TimerId: 1. Counter: 8, Time: 23323. Process WM_TIMER. TimerId: 2. Counter: 9, Time: 24337. Process WM_TIMER. TimerId: 1. Counter: 9, Time: 24337. Process WM_TIMER. TimerId: 2. Counter: 10, Time: 25351. Process WM_TIMER. TimerId: 1. Counter: 10, Time: 25351.
|
结果跟第二个场景一模一样,可见即使是不同窗口中的定时器,WM_TIMER消息也不会堆积。
总结
经过以上三个场景的试验,可以得出这个结论:同一个定时器的WM_TIMER消息在消息队列中至多存在一个,不会堆积。
要注意的是,即使WM_TIMER消息不会堆积,在使用定时器时仍然要小心避免处理时间比间隔时间长的情况。由试验结果可以看到,一旦出现这种情况,消息队列中总会存在一个WM_TIMER消息等待处理,程序会忙于处理这些WM_TIMER消息,一刻都不停歇,就像陷入了一个循环,这对程序有严重的影响。
由于各种因素的影响,对于同样的处理逻辑,每次执行所用的时间很可能都不一样。所以,如果担心处理时间过长,可以通过更安全的方式来使用定时器,即模拟一次性定时器:在开始处理WM_TIMER消息的时候,调用KillTimer停止定时器;处理完成之后,再调用SetTimer重新开启定时器。例如,把第一个场景中处理WM_TIMER的代码改成以下的安全方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) { case WM_TIMER: {
KillTimer(hwnd, kTimerId);
std::wstringstream stream; stream << L"Process WM_TIMER. " << L"TimerId: " << wParam << ". " << L"Counter: " << ++g_counter << ", " << L"Time: " << GetTickCount() - g_begin_tick_count << '.' << std::endl;
std::wstring string = stream.str(); OutputDebugString(string.c_str());
if (g_counter < 5) { Sleep(5000); }
SetTimer(hwnd, kTimerId, 1000, nullptr); return 0; }
default: return CallWindowProc(DefWindowProc, hwnd, message, wParam, lParam); } }
|
启动程序,静置一段时间之后,输出如下:
1 2 3 4 5 6 7 8 9 10 11
| Process WM_TIMER. TimerId: 1. Counter: 1, Time: 1014. Process WM_TIMER. TimerId: 1. Counter: 2, Time: 7036. Process WM_TIMER. TimerId: 1. Counter: 3, Time: 13058. Process WM_TIMER. TimerId: 1. Counter: 4, Time: 19079. Process WM_TIMER. TimerId: 1. Counter: 5, Time: 25101. Process WM_TIMER. TimerId: 1. Counter: 6, Time: 26115. Process WM_TIMER. TimerId: 1. Counter: 7, Time: 27129. Process WM_TIMER. TimerId: 1. Counter: 8, Time: 28143. Process WM_TIMER. TimerId: 1. Counter: 9, Time: 29157. Process WM_TIMER. TimerId: 1. Counter: 10, Time: 30171. Process WM_TIMER. TimerId: 1. Counter: 11, Time: 31185.
|
通过这种方式,不管处理时间有多长,在处理完一个WM_TIMER消息之后,总会真正等待1秒才处理下一个,避免了程序长时间处于繁忙状态的情况。