C#使用PostMessage討論

SendMessage、PostMessage

1、SendMessage、PostMessage的運行機制。
2、SendMessage、PostMessage的運行內幕。
3、SendMessage、PostMessage的內部實現。

注:須先瞭解Windows的消息迴圈機制

1、SendMessage、PostMessage的運行機制



SendMessage可以理解為,SendMessage函數發送消息,等待消息處理完成後,SendMessage才返回

稍微深入一點,等待視窗處理函數返回後,SendMessage就返回了

PostMessage可以理解為,PostMessage函數發送消息,不等待消息處理完成,立刻返回

稍微深入一點,

PostMessage只管發送消息,消息有沒有被送到則並不關心
只要發送了消息,便立刻返回。

對於寫一般Windows程式的程式師來說,能夠這樣理解也就足夠了。

但SendMessage、PostMessage真的是一個發送消息等待、一個發送消息不等待嗎?

2、SendMessage、PostMessage的運行內幕

在寫一般Windows程式時,如上第1點講到的足以應付,

其實我們可以看看MSDN來確定SendMessage、PostMessage的運行內幕。

在MSDN中,SendMessage解釋如為:

The SendMessage function sends the specified message to a window or windows. It calls the window procedure for the specified window and does not return until the window procedure has processed the message.

翻譯成中文為:

SendMessage函數將指定的消息發到視窗。它調用特定視窗的視窗處理函數,並且不會立即返回,直到視窗處理函數處理了這個消息。

再看看PostMessage的解釋:

The PostMessage function places (posts) a message in the message queue associated with the thread that created the specified window and returns without waiting for the thread to process the message.

    翻譯成中文為:

PostMessage函數將一個消息放入與創建這個視窗的消息佇列相關的線程中,並立刻返回不等待線程處理消息。
仔細看完MSDN解釋,我們瞭解到,SendMessage的確是發送消息,然後等待處理完成返回,但發送消息的方法為直接調用消息處理函數(即WndProc函數),按照函數調用規則,肯定會等消息處理函數返回之後,SendMessage才返回。

而PostMessage卻沒有發送消息,PostMessage是將消息放入消息佇列中,然後立刻返回,至於消息何時被處理,PostMessage完全不知道,此時只有消息迴圈知道被PostMessage的消息何時被處理了。

至此我們撥開了一層疑雲,原來SendMessage只是調用我們的消息處理函數,PostMessage只是將消息放到消息佇列中。下一節將會更深入這兩個函數,看看Microsoft究竟是如何實現這兩個函數的。

3、SendMessage、PostMessage的內部實現

Windows內部運行原理、機制往往是我們感興趣的東西,而這些東西又沒有被文檔化,所以我們只能使用Microsoft提供的工具自己研究了。
首先,在基本Win32工程代碼中,我們可以直接看到消息處理函數、消息迴圈,所以建立一個基本Win32工程(本篇文章使用VS2005),為了看到更多資訊,我們需要進行設置,讓VS2005載入Microsoft的Symbol(pdb)文件[1]。為了方便,去除了一些多餘的代碼,加入了兩個功能表,ID分別為:
IDM_SENDMESSAGE、
IDM_POSTMESSAGE。

如下列出經過簡化後的必要的代碼。

消息迴圈:

Ln000:while (GetMessage(&msg, NULL, 0, 0))
Ln001:{
Ln002:    TranslateMessage(&msg);
Ln003:    DispatchMessage(&msg);
Ln004:}

消息處理函數:

Ln100:LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
Ln101:{
Ln102:    int wmId, wmEvent;
Ln103:    switch (message)
Ln104:    {
Ln105:    case WM_COMMAND:
Ln106:        wmId = LOWORD(wParam);
Ln107:        wmEvent = HIWORD(wParam);
Ln108:        switch (wmId)
Ln109:        {
Ln110:        case IDM_EXIT:
Ln111:            DestroyWindow(hWnd);
Ln112:            break;
Ln113:        case IDM_SENDMESSAGE:
Ln114:            SendMessage(hWnd, WM_SENDMESSAGE, 0, 0);
Ln115:            break;
Ln116:        case IDM_POSTMESSAGE:
Ln117:            PostMessage(hWnd, WM_POSTMESSAGE, 0, 0);
Ln118:            break;
Ln119:        default:
Ln120:            return DefWindowProc(hWnd, message, wParam, lParam);
Ln121:        }
Ln122:        break;
Ln123:
Ln124:    case WM_SENDMESSAGE:
Ln125:        MessageBox(hWnd, L"SendMessage", L"Prompt", MB_OK);
Ln126:        break;
Ln127:
Ln128:    case WM_POSTMESSAGE:
Ln129:        MessageBox(hWnd, L"PostMessage", L"Prompt", MB_OK);
Ln130:        break;
Ln131:
Ln132:    case WM_DESTROY:
Ln133:        PostQuitMessage(0);
Ln134:
Ln135:    default:
Ln136:        return DefWindowProc(hWnd, message, wParam, lParam);
Ln137:    }
Ln138:    return 0;
Ln139:}


下面一步步分析這兩個函數的內部情況,先討論 SendMessage。

第一步,在DispatchMessage(Ln003)函數處下個中斷點,F5進行調試,當程式運行到中斷點後,查看 CallStack 視窗,可得如下結果:

#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, int nCmdShow=0x00000001)  Line 49   C++
#002:MyProj.exe!__tmainCRTStartup()  Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup()  Line 414 C
#000:kernel32.dll!_BaseProcessStart@4()  + 0x23 bytes

我們可以看到,進程先調用 kernel32.dll 中的 BaseProcessStart 函數,然後調用的 Startup Code 的函數 wWinMainCRTStartup,然後調用 _tmainCRTStartup 函數,最終調用我們的 wWinMain 函數,我們的程式就運行起來了。

第二步,去除第一步下的中斷點,在 WndProc(Ln101) 函數入口處下個中斷點,F5 繼續運行,運行到新下的中斷點處,查看 CallStack 視窗,可得如下結果:

#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000101, unsigned int wParam=0x00000074, long lParam=0xc03f0001)  Line 122    C++
#007:user32.dll!_InternalCallWinProc@20()  + 0x28 bytes 
#006:user32.dll!_UserCallWinProcCheckWow@32()  + 0xb7 bytes 
#005:user32.dll!_DispatchMessageWorker@8()  + 0xdc bytes
#004:user32.dll!_DispatchMessageW@4()  + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, #000:int nCmdShow=0x00000001)  Line 49 + 0xc bytes C++
#002:MyProj.exe!__tmainCRTStartup()  Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup()  Line 414 C
#000:kernel32.dll!_BaseProcessStart@4()  + 0x23 bytes

    #000~#003 跟第一步相同,不再解釋。在 #004、#005,可以看到,函數運行到 DispatchMessage 的內部了,DispatchMessageW、DispatchMessageWorker 是 user32.dll 中到處的函數,而且函數前部字串相等,在此猜想應該是 DispatchMessage 的內部處理。

#008 為我們消息處理函數,所以推想而知,#006、#007 是為了調用我們的消息處理函數而準備的代碼。

第三步,去除第二步下的中斷點,在Ln003、Ln114、Ln115、Ln125 處分別下一個中斷點,在功能表中選擇對應項,使程式運行至 Ln114,F10下一步,可以看到並沒有運行到 break(Ln115),直接跳到了 Ln125 處,由此可知目前 SendMessage 已經在等待了,查看 CallStack 視窗,可得如下結果:

#013:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000500, unsigned int wParam=0x00000000, long lParam=0x00000000)  Line 147    C++
#012:user32.dll!_InternalCallWinProc@20()  + 0x28 bytes 
#011:user32.dll!_UserCallWinProcCheckWow@32()  + 0xb7 bytes 
#010:user32.dll!_SendMessageWorker@20()  + 0xc8 bytes
#009:user32.dll!_SendMessageW@16()  + 0x49 bytes
#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000111, unsigned int wParam=0x00008003, long lParam=0x00000000)  Line 136 + 0x15 bytes   C++
#007:user32.dll!_InternalCallWinProc@20()  + 0x28 bytes 
#006:user32.dll!_UserCallWinProcCheckWow@32()  + 0xb7 bytes 
#005:user32.dll!_DispatchMessageWorker@8()  + 0xdc bytes
#004:user32.dll!_DispatchMessageW@4()  + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, #000:int nCmdShow=0x00000001)  Line 49 + 0xc bytes C++
#002:MyProj.exe!__tmainCRTStartup()  Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup()  Line 414 C
#000:kernel32.dll!_BaseProcessStart@4()  + 0x23 bytes

#000~#008 跟上面的相同,不再解釋。
在 #009、#010,可以看到,函數調用到 SendMessage 內部了,在此猜想應該是 SendMessage 的內部處理。
#011、#012 跟第二步中的 #006、#007 一樣,
在第二步中,這兩個函數是為了調用消息處理函數而準備的代碼
#013 也是我們的消息處理函數,所以此兩行代碼的功能相等。

至此,我們證明了 SendMessage 的確是直接調用消息處理函數的,在消息處理函數返回前,SendMessage 等待。在所有的操作中,Ln003 中斷點沒有去到,證明 SendMessage 不會將消息放入消息佇列中(在 PostMessage 分析中,此中斷點將會跑到,接下來講述)。

第四步,F5繼續運行,此時彈出對話方塊,點擊對話方塊中的確定後,運行到中斷點 Ln115 處。查看 CallStack 窗口,可得如下結果:

#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000111, unsigned int wParam=0x00008003, long lParam=0x00000000)  Line 137    C++
#007:user32.dll!_InternalCallWinProc@20()  + 0x28 bytes 
#006:user32.dll!_UserCallWinProcCheckWow@32()  + 0xb7 bytes 
#005:user32.dll!_DispatchMessageWorker@8()  + 0xdc bytes
#004:user32.dll!_DispatchMessageW@4()  + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, int nCmdShow=0x00000001)  Line 49 + 0xc bytes   C++
#002:MyProj.exe!__tmainCRTStartup()  Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup()  Line 414 C
#000:kernel32.dll!_BaseProcessStart@4()  + 0x23 bytes

#000~008 跟第二步的完全相同,此時 SendMessage 也已經返回,所調用的堆疊也清空了。
至此,我們徹底撥開了 SendMessage 的疑雲,瞭解了 SendMessage 函數的運行機制,綜述為,SendMessage 內部調用 SendMessageW、SendMessageWorker 函數做內部處理,然後調用 UserCallWinProcCheckWow、InternalCallWinProc 來調用我們代碼中的消息處理函數,消息處理函數完成之後,SendMessage 函數便返回了。

SendMessage 討論完之後,現在討論 PostMessage,將上面的所有中斷點刪除,關閉調試。

第一步,在DispatchMessage(Ln003)函數處下個中斷點,F5進行調試,此處結果跟 SendMessage 一樣,不再說明。

第二步,去除第一步下的中斷點,在 WndProc(Ln101) 函數入口處下個中斷點,F5 繼續運行,此處結果跟 SendMessage 一樣,不再說明。

第三步,去除第二步下的中斷點,在 Ln003、Ln117、Ln118、Ln129 處分別下一個中斷點,在功能表中選擇對應項,使程式運行至 Ln117,F10 下一步,可以看到已經運行到 break,PostMessage 函數返回了,此時 CallStack 沒有變化。

第四步,F5 繼續運行,此時程式運行到 Ln003,CallStack 跟第一步剛起來時一樣。

第五步,F5 繼續運行(由於有多個消息,可能要按多次),讓程式運行到 Ln129,此時
CallStack 跟第二步相同,為了方便說明,再次列舉如下:

#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00070874, unsigned int message=0x00000501, unsigned int wParam=0x00000000, long lParam=0x00000000)  Line 151    C++
#007:user32.dll!_InternalCallWinProc@20()  + 0x28 bytes 
#006:user32.dll!_UserCallWinProcCheckWow@32()  + 0xb7 bytes 
#005:user32.dll!_DispatchMessageWorker@8()  + 0xdc bytes
#004:user32.dll!_DispatchMessageW@4()  + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, int nCmdShow=0x00000001)  Line 49 + 0xc bytes   C++
#002:MyProj.exe!__tmainCRTStartup()  Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup()  Line 414 C
#000:kernel32.dll!_BaseProcessStart@4()  + 0x23 bytes

由此可以看到,此調用是從消息迴圈中調用而來,
DispatchMessageW、DispatchMessageWorker 是 DispatchMessage 的內部處理,UserCallWinProcCheckWow、InternalCallWinProc是為了調用我們的消息處理函數而準備的代碼。

至此,我們再次徹底撥開了 PostMessage 的疑雲,瞭解了 PostMessage 函數的運行機制,綜述為,PostMessage 將消息放入消息佇列中,自己立刻返回,消息迴圈中的 GetMessage(PeekMessage 也可,本例中為演示)處理到我們發的消息之後,便按照普通消息處理方法進行處理。


windows消息机制的有趣发现(一)
作者:CharlesSimonyi 2013-04-01

标签 消息 队列 10000 NULL 线程



最近琢磨windows的消息机制,发现一些有趣的地方,可能是对的,也可能是错的,分享出来让大家评判评判。

1:每个线程都有消息队列

不仅仅是有窗口的线程,没有窗口的线程也有消息队列!编写一个win32控制台程序,代码如下:

#include<windows.h>

void main()

DWORD dwThread = GetCurrentThreadId();
::PostThreadMessage(dwThread,10000,NULL,NULL);
MSG msg;
GetMessage(&msg,NULL,NULL,NULL);
}


获取当前线程ID,对当前线程发送线程消息,然后调用GetMessage函数从消息队列中取消息,看看能不能取到我们之前发出的编号为10000的消息。



运行发现GetMessage函数成功从消息队列中取出了我们发送的编号为10000的消息!我们这个线程没有窗口、没有消息循环,但是有消息队列,消息队列是windows为线程提供的。

2:消息队列的容量有多大?

如果我们不停的发送消息,却不用GetMessage之类的函数获取并从消息队列中移除消息。消息队列中的消息岂不是越来越多?那么这个消息队列究竟有多大容量呢?它最多能容纳多少消息?我们编写如下代码

#include<windows.h>

void main()

DWORD dwThread = GetCurrentThreadId();
for (int i=0;i<1000;i++)
PostThreadMessage(dwThread,10000,NULL,NULL);
int n = 0;
MSG msg;
while(PeekMessage(&msg,NULL,NULL,NULL,PM_REMOVE))
{
if (msg.message == 10000)
n++;
}

}
循环调用PostThreadMessage发送1000个消息,然后用PeekMessage取出消息,最后一个参数PM_REMOVE说明每取出一个消息后将该消息从消息队列中移除。我们用变量n来统计从消息队列中取出了多少个编号为10000的消息。这里为什么不用GetMessage呢?因为GetMessage把消息队列中的消息取完了以后会阻塞起来一直等待,我们的while循环无法退出,后面的断点也无法命中。而PeekMessage把消息队列中的消息取完了以后,取不到消息就返回0,我们的while也就结束了。



运行后发现,果然,n的值为1000,取出了1000个我们发送的消息。接下来我们再增大消息的发送数量,看看还能取出多少。



这次我们发送了50000个消息,但是运行后发现只从消息队列中取出了10000个消息!消息队列的容量为10000个吗?我们上网查资料,确实如此。消息队列的容量默认为10000个,这个值可以在注册表中修改。

展开注册表:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\

找到USERPostMessageLimit 项,发现该项的默认值为10000。我们可以通过修改该值来改变消息队列的容量,重启后生效。



留言

熱門文章