Windows消息調度機制和線程同步控制

原地址:

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 參數標識的窗口。然後系統分配一塊內存,將這個消息參數存儲在這塊內存中,並將這塊內存增加到相應線程的登記消息隊列中。並且該函數還設置QS_POSTMESSAGE喚醒位。函數 PostMesssage 在登記了消息後立即返回,調用該函數的線程不知道登記的消息是否被指定窗口的窗口過程所處理。

還可通過調用函數

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 toone of the User or Windows Graphics Device Interface (GDI) functions", 這句話的意思是不是等同於創建一個窗口呢?

 


發佈了13 篇原創文章 · 獲贊 2 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章