Windows訊息排程機制和執行緒同步控制
訊息分為進隊訊息訊息和非進隊訊息。所謂進隊訊息就是windows將訊息傳送到每個執行緒所專有的佇列中,然後由程式自主處理,這種訊息基本上是由使用者輸入產生(wm_keydown,wm_keyup,wm_char,wm_mouse**,以及wm_paint,wm_timer,wm_quit)或者是呼叫postmessage,postthreadmessage產生的訊息;所謂的非進隊訊息就是直接傳送給視窗過程的訊息,就是直接呼叫視窗過程,上述訊息以外的一般都是這種型別!
一個執行緒一旦建立了至少一個視窗,則系統就為其分配一個訊息佇列。主要表現形式為系統為其分配一個THREADINFO結構,該結構有四個指標分別指向登記訊息佇列,傳送訊息佇列,應答訊息佇列和虛擬輸入佇列。如果想將訊息放入登記訊息佇列,可以呼叫postmessage,或者postthreadmessage。其餘的訊息佇列主要用於處理如下的事務。當某執行緒呼叫sendmessage給別的執行緒建立的視窗時,傳送的訊息首先追加到接收執行緒的傳送訊息佇列,傳送執行緒處於空閒狀態,等待接收執行緒處理完他的訊息返回給傳送執行緒的應答佇列,等到後傳送執行緒被喚醒取得應答佇列的訊息(就是處理完訊息的返回值),繼續執行。而虛擬輸入佇列則是由windows的系統執行緒RIT(原始輸入執行緒)負責將硬體事件轉換成訊息新增到對應執行緒的虛擬訊息佇列中。
處理訊息佇列的順序。首先windows絕對不是按佇列先進先出的次序來處理的,而是有一定優先順序的。優先順序通過訊息佇列的狀態標誌來實現的。首先最高優先順序的是別的執行緒發過來的訊息(通過sendmessage),其次是處理登記訊息佇列訊息,再次處理QS_QUIT標誌,再處理虛擬輸入佇列,再處理wm_paint最後是wm_timer!
Windows這個作業系統是靠訊息來驅動的,而且只有窗體才能接收訊息,我們經常見到的窗體、按鈕、文字框等這都是窗體,為了能夠讓窗體接受訊息,對應於每一個窗體都有一個回撥WndProc函式,Windows系統負責在必要的時候(有訊息到達的時候)呼叫這個回撥函式。
執行緒與訊息佇列
當一個執行緒第一次被建立時,系統假定執行緒不會用於任何與使用者相關的任務。這樣可以減少執行緒對系統資源的要求。但是,一旦該執行緒呼叫一個與圖形使用者介面有關的函式 ( 如檢查它的訊息佇列或建立一個視窗 ),系統就會為該執行緒分配一些另外的資源,以便它能夠執行與使用者介面有關的任務。特別是,系統分配了一個THREADINFO結構,並將這個資料結構與執行緒聯絡起來。
THREADINFO結構體如下:
1.將訊息傳送到執行緒的訊息佇列
當執行緒有了與之聯絡的THREADINFO結構時,訊息就有自己的訊息佇列集合。
通過呼叫函式 BOOL PostMesssage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
可以將訊息放置線上程的登記訊息佇列中。
當一個執行緒呼叫這個函式時,系統要確定是哪個執行緒建立了用 hwnd
還可通過呼叫函式 BOOL PostThreadMesssage(DWORD dwThreadId, UINT uMsg, WPARAM wParam, LPARAM lParam) 將訊息放置線上程的登記訊息佇列中,同 PostMesssage 函式一樣,該函式在向執行緒的佇列登記訊息後立即返回,呼叫該函式的執行緒不知道訊息是否被處理。
向執行緒的佇列傳送訊息的函式還有 VOID PostQuitMesssage(int nExitCode) ;
該函式可以終止執行緒訊息的迴圈,呼叫該函式類似於呼叫:PostThreadMesssage(GetCurrenThreadId( ), WM_QUIT, nExitCode, 0); 但 PostQuitMesssage 並不實際登記一個訊息到任何佇列中。只是在內部,該函式設定 QS_QUIT 喚醒標誌,並設定 THREADINFO 結構的 nExitCode 成員。
執行緒間用postmessage通訊和執行緒同步控制LOCK()攪絆在一起容易出問題,如造成主執行緒進度條無法進度重新整理等,出現短暫的死鎖掛起等現象。
2.向視窗傳送訊息
將視窗訊息直接傳送給一個視窗過程可以使用函式 LRESULT SendMessage( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 視窗過程將處理這個訊息,只有當訊息被處理後,該函式才能返回。即具有同步的特性。
該函式的工作機制:
2.1 如果呼叫該函式的執行緒向該執行緒所建立的視窗傳送了一個訊息,SendMessage 就很簡單:它只是呼叫指定視窗的視窗過程,將其作為一個子例程。當視窗過程完成對訊息的處理時,它向 SendMessage 返回一個值。SendMessage 再將這個值返回給呼叫執行緒。
2.2 當一個執行緒向其他執行緒所建立的視窗傳送訊息時,SendMessage 就複雜很多(即使兩個執行緒在同一個程序中也是如此)。windows 要求建立視窗的執行緒處理視窗的訊息。所以當一個執行緒呼叫 SendMessage 向一個由其他程序所建立的視窗傳送一個訊息,也就是向其他執行緒傳送訊息,傳送執行緒不可能處理該視窗訊息,因為傳送執行緒不是執行在接收程序的地址空間中,因此不能訪問相應視窗的過程的程式碼和資料。(對於這個,我有點疑問:同一個程序的不同執行緒是執行在相同程序的地址空間中,它也採用這種機制,又作何解釋呢?)實際上,傳送執行緒要掛起,而有另外的執行緒處理訊息。所以為了向其他執行緒建立的視窗傳送一個視窗訊息,系統必須執行一些複雜的動作。
由於windows使用上述方法處理執行緒之間的傳送訊息,所以有可能造成執行緒掛起,嚴重的會出現死鎖。
利用一下4個函式可以編防寫性程式碼防護出現這種情況。
1. LRESULT SendMessageTimeout( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT fuFlags, UINT uTimeout , PDWORD_PTR pdwResult);
2. BOOL SendMessageCallback( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, SENDSYNCPROC pfnResultCallback, ULONG_PTR dwData);
3. BOOL SendNotifyMessage( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
4.BOOL ReplyMessage( LRESULT lResult);
另外可以使用函式 BOOL InSendMessage( ) 判斷是在處理執行緒間的訊息傳送,還是在處理執行緒內的訊息傳送 。
window 作業系統猜測。
2.每當一個執行緒建立一個視窗的時候,作業系統內部都會把該視窗的Handle和執行緒相關聯。很有可能在作業系統內部會維護一個視窗handle到執行緒的map. 還有一種可能就是視窗的成員變數裡面有一個指標,指向建立它的執行緒。
3.視窗本身並沒有訊息佇列,所有發到視窗的訊息,都會自動被髮到建立該視窗的執行緒的訊息佇列中。
4.每個執行緒只能處理自己執行緒佇列裡面的訊息,不能處理其他執行緒訊息佇列裡面的訊息。
所以PeekMessage(LPMSG lpMsg, HWND hWnd, UINT,UINT,UINT)函式中,如果hWnd不是本執行緒建立的視窗,則該函式呼叫失敗。
5.由於線上程訊息佇列裡面的訊息會包含有視窗控制代碼,所以PeekMessage可以專門處理某個特殊視窗的訊息。
1. 曾經有疑問執行緒是不是隻有建立了窗口才具有訊息佇列,但又覺得應該不是這樣,因為在windows的API裡面有個函式叫PostThreadMessage,可以直接把訊息投遞到執行緒的訊息佇列裡面,而不需要任何視窗控制代碼。後來在MSDN裡面有這麼一段描述,覺得解釋的很詳細:
這裡唯一的疑問我想應該是”makes its first call to one of the User or Windows Graphics Device Interface (GDI) functions", 這句話的意思是不是等同於建立一個視窗呢?