WM_TIMER消息是否会在消息队列中堆积

在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_counter1g_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秒才处理下一个,避免了程序长时间处于繁忙状态的情况。