【詳解】消息隊列和線程關系


1.進程-線程-消息隊列

 

簡單的來說,什么是進程?什么是線程?打個比方,你的程序要執行,操作系統就會把你的exe文件加載到內存中,那就生成一個進程了(當然還包含分配到的資源等);對於線程,你可以理解成是一個程序里的不同部分,這有點類似函數,所不同的是各個線程是同時執行的。

 

例如,你的主線程創建了另一個副線程,那么這兩個線程是同時在工作的,不存在調用 - 返回的概念。

 

一個進程里可以有多個線程在執行,稱為執行實例。

 

shining:因為線程的資源是從進程資源中分配出來的,因此同一個進程中的多個線程會有共享的內存空間,這樣可能會引起多個線程的同步問題,調度不好,就會-出問題,比如A線程要用的資源必須等待B線程釋放,而B也在等待其他資源釋放才能繼續。這就是有些網友碰見的問題:同一個測試場景,用線程並發就會超時失敗或報-錯,而用進程並發就沒錯。這是因為進程影響互相極小,當然開進程的資源消耗就比較大,這是一個副作用。

 

根據我的理解,進程應該是比較大的概念,一個進程開始時至少會有一個主線程 ( 即主執行實例 ) ,這是在系統加載你的程序的時候所創建的主執行流程。一般對外部來說只能看到進程,例如在Win2000 的任務管理器里面查看到的只有進程 ( Process ) 而已。用 Ctrl + Shift + ESC 可以在 Win2000里調出任務管理器。而消息隊列則是與線程 ( Thread ) 相關的,換句話說,一個線程只能有一個消息隊列 (queue) 與之相對應。這跟之前說的有點不同,一個進程里面可以有多個線程;但是一線程里面就不能超出一個消息隊列( Win98 里面甚至可以沒有消息隊列 )

 

消息隊列是在什么時候生成的呢?在 Win2000里面,從一開始創建線程就已經有了。( Win98里,我估計是在創建過窗口之后,留給你去證實 )

說了半天,可能一些剛入門的朋友還不知道什么是消息隊列呢?

其實,Windows操作系統是一個基於事件驅動的系統。它把握諸如鼠標,鍵盤輸入等東西化為事件代號,發送到你的程序的消息隊列里面去,你的程序則每次提取一個事件,根據事件的性-質執行相應的操作,不斷循環而已。

 

shining notes:消息機制在unix平台下也是存在的,是進程間通訊的方式,一般由操作系統來提供。微軟提倡編程人員使用事件驅動的編程方法。你也可以向自己線程的消息隊列里發送假消息,自己騙自己也是可以的( 虛偽)!使用 PostThreadMessage 函數即可。

 

編出多線程的程序其實並不難,難點其實在於線程同步 ( 線程間協調工作 ),下面的源程序正是為了簡單介紹多線程編程的。

( 閱讀的時候不要忘了主線程的入口是 WinMain 函數 )

// File Name: WinMain.cpp

#define WIN32_LEAN_AND_MEAN // Say No to MFC !!

#include <windows.h>

char Temp[77] = "";

// 我自定義的新線程入口 MyThread 函數

DWORD WINAPI MyThread( LPVOID lpParameter )

{

long ThrVal = 1 ;

for (ThrVal = 1; ThrVal < 16; ThrVal++ )

{

wsprintf( Temp , "這是副線程第 %ld 次顯示" , ThrVal );

MessageBox( NULL, Temp, "第二線程內容 __CopyRight -

`海風 ", MB_OK | MB_TOPMOST );

}

return 1 ;

} // 副線程結束

 

// Name: WinMain() 主線程的入口

// ------ ---------- ----------- ---------

int WINAPI WinMain( HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR lpCmdLine, int nCmdShow )

{

DWORD dwThreadId, dwMyThrdParam = 1; //

第一個參數是新線程的 ID 號,第二個參數略

HANDLE hThread; // 副線程的 handle

 

// 調用函數創建新的線程,新線程入口是 MyThread函數

hThread = CreateThread( NULL, // 沒有(或默認) 的屬性

0, // 使用默認堆棧大小

MyThread, // 我的線程入口函數

&dwMyThrdParam, // argument to thread function

0, // 使用默認 creation flags

&dwThreadId); // Win98里不能省略

 

// 已經創建了新的副線程

MessageBox( NULL, "已經創建了新的副線程、\n" " 按確定結束主線程!",

"主線程信息顯示 __CopyRight - `海風 ", MB_OK | MB_TOPMOST );

 

ExitProcess(0);

return NULL;

}

 

如果說消息隊列只有一個(在一個線程內),那么消息隊列可以容納多少條消息呢?我編程序去驗證了一下:

// File Name: WinMain.cpp

 

#define WIN32_LEAN_AND_MEAN // Say No to MFC !!

#include <windows.h>

char Temp[77]="Hello world";

// Name: WinMain()

// ------ ---------- ----------- ---------

int WINAPI WinMain( HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR lpCmdLine, int nCmdShow )

{

MessageBox( NULL, "按確定開始測試", "Simple_Code__CopyRight - `海風 ",

MB_OK | MB_TOPMOST );

//以上的一句請使用 Win98的朋友別刪除,因為在調用過創建窗口之類的函數后,你的線程才具有消息隊列!

DWORD CurThreadID = GetCurrentThreadId( ); // 取得當前線程的ID (標識號)

long i = 1 ;

for ( i = 1; i < 900000; i++)

{

if (! PostThreadMessage( CurThreadID, WM_USER , 11, 22 ) )

break;

// 上面的一句是如果不能再添加消息就打斷 for 循環

}

 

wsprintf (Temp, "具體的消息隊列長度是 %ld ", i);

 

MessageBox( NULL, Temp, "Sample_Code__CopyRight - `海風 ", MB_OK | MB_TOPMOST );

 

ExitProcess(0);

return NULL;

}

 

顯示的結果是: 10000 ,有那么大么?!我也有點不太相信。

如果 10000 都還不夠用,那么會怎樣?

哎,當然是丟失了!

 

為了證明消息隊列是線程的附屬品,查看了MSDN ,最后在 PostThreadMessage() 函數的第一個參數解釋那里找到證據,原文如下:

" The function fails if the specified thread does not have a message queue. The system creates a thread's message queue when the thread makes its first call to one of the Win32 User or GDI functions. For more information, see the Remarks section. "

含義大概是,當一個線程里面第一次調用 Win32 User 類函數 (或圖形界面類函數)的時候,系統會為該線程創建一個消息隊列,否則就沒有消息隊列。

shining notes:個人認為,消息隊列其實和線程並無任何聯系,只是windows系統將二者結合到了一起,成為windows系統的處處可見的消息機制。在 unix-系統下,我看到的是:如果哪個程序需要接受消息,它將獲得一個mailbox(不用向系統申請,可直接調用系統層次的API即可),並不斷輪詢(一個forever的循環)去查收這個mail box是否有消息。這應該是程序和消息隊列結合的雛形吧。有不對的地方,請各位指出。

不過,這個說法我認為在 Win2000里面是不適用的,其實在系統創建線程的同時就創建了一個相關消息隊列(默認操作)

我嘗試過沒有調用任何其他函數的情況下也可以成功發送消息到隊列。

這個由系統維護的消息隊列也挺自動化的!舉個簡單例子是系統會自動調整 WM_PAINT 消息的數量使之不會重復;

更復雜一點的例子是你向線程里的一個窗口發送消息,然后Destroy 那個窗口,最后才檢測消息隊列。系統會認為那個窗口已經不存在而將與那窗口相關的多余消息一並刪除掉了,所以你一定沒法在消息隊列里找到先前你發送的那條消息。

 

2.線程與消息隊列

      當一個線程第一次被創建時,系統假定線程不會用於任何與用戶相關的任務。這樣可以減少線程對系統資源的要求。但是,一旦該線程調用一個與圖形用戶界面有關的函數 ( 如檢查它的消息隊列或建立一個窗口 ),系統就會為該線程分配一些另外的資源,以便它能夠執行與用戶界面有關的任務。特別是,系統分配了一個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 成員。

 

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( ) 判斷是在處理線程間的消息發送,還是在處理線程內的消息發送

 

3CreateEventSetEvent函數

當你創建一個線程時,其實那個線程是一個循環,不像上面那樣只運行一次的。這樣就帶來了一個問題,在那個死循環里要找到合適的條件退出那個死循環,那么是怎么樣實現它的呢?在Windows里往往是采用事件的方式,當然還可以采用其它的方式。在這里先介紹采用事件的方式來通知從線程運行函數退出來,它的實現原理是這樣,在那個死循環里不斷地使用WaitForSingleObject函數來檢查事件是否滿足,如果滿足就退出線程,不滿足就繼續運行。當在線程里運行阻塞的函數時,就需要在退出線程時,先要把阻塞狀態變成非阻塞狀態,比如使用一個線程去接收網絡數據,同時使用阻塞的SOCKET時,那么要先關閉SOCKET,再發送事件信號,才可以退出線程的。下面就來演示怎么樣使用事件來通知線程退出來。

 

函數CreateEvent聲明如下:

 

WINBASEAPI

__out

HANDLE

WINAPI

CreateEventA(

    __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes,

    __in     BOOL bManualReset,

    __in     BOOL bInitialState,

    __in_opt LPCSTR lpName

    );

WINBASEAPI

__out

HANDLE

WINAPI

CreateEventW(

    __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes,

    __in     BOOL bManualReset,

    __in     BOOL bInitialState,

    __in_opt LPCWSTR lpName

    );

#ifdef UNICODE

#define CreateEvent CreateEventW

#else

#define CreateEvent CreateEventA

#endif // !UNICODE

lpEventAttributes是事件的屬性。

bManualReset是指事件手動復位,還是自動復位狀態。

bInitialState是初始化的狀態是否處於有信號的狀態。

lpName是事件的名稱,如果有名稱,可以跨進程共享事件狀態。

 

調用這個函數的例子如下:

#001 #pragma once

#002

#003 //線程類。

#004 //蔡軍生 2007/09/23 QQ:9073204

#005 class CThread

#006 {

#007 public:

#008

#009  CThread(void)

#010  {

#011          m_hThread = NULL;

#012          m_hEventExit = NULL;

#013  }

#014

#015  virtual ~CThread(void)

#016  {

#017         if (m_hThread)

#018        {

#019               //刪除的線程資源。

#020               ::CloseHandle(m_hThread);

#021         }

#022

#023         if (m_hEventExit)

#024         {

#025               //刪除事件。

#026               ::CloseHandle(m_hEventExit);

#027         }

#028       

#029  }

#030

#031  //創建線程

#032  HANDLE CreateThread(void)

#033  {

#034         //創建退出事件。

#035         m_hEventExit = ::CreateEvent(NULL,TRUE,FALSE,NULL);

#036         if (!m_hEventExit)

#037          {

#038               //創建事件失敗。

#039               return NULL;

#040         }

#041

#042         //創建線程。

#043          m_hThread = ::CreateThread(

#044               NULL,                    //安全屬性使用缺省。

#045               0,                         //線程的堆棧大小。

#046               ThreadProc,                 //線程運行函數地址。

#047               this,                      //傳給線程函數的參數。

#048               0,                         //創建標志。

#049               &m_dwThreadID);        //成功創建后的線程標識碼。

#050

#051         return m_hThread;

#052  }

#053

#054  //等待線程結束。

#055  void WaitFor(DWORD dwMilliseconds = INFINITE)

#056  {

#057         //發送退出線程信號。

#058        ::SetEvent(m_hEventExit);

#059

#060         //等待線程結束。

#061         ::WaitForSingleObject(m_hThread,dwMilliseconds);

#062  }

#063

#064 protected:

#065  //

#066  //線程運行函數。

#067  //蔡軍生 2007/09/21

#068  //

#069  static DWORD WINAPI ThreadProc(LPVOID lpParameter)

#070  {

#071         //轉換傳送入來的參數。

#072         CThread* pThread = reinterpret_cast<CThread *>(lpParameter);

#073         if (pThread)

#074         {

#075               //線程返回碼。

#076               //調用類的線程處理函數。

#077               return pThread->Run();

#078         }

#079       

#080         //

#081         return -1;      

#082  }

#083

#084  //線程運行函數。

#085  //在這里可以使用類里的成員,也可以讓派生類實現更強大的功能。

#086  //蔡軍生 2007/09/25

#087  virtual DWORD Run(void)

#088  {

#089         //輸出到調試窗口。

#090         ::OutputDebugString(_T("Run()線程函數運行\r\n"));    

#091

#092         //線程循環。

#093         for (;;)

#094         {

#095              DWORD dwRet = WaitForSingleObject(m_hEventExit,0);

#096               if (dwRet == WAIT_TIMEOUT)

#097               {

#098                    //可以繼續運行。               

#099                    TCHAR chTemp[128];

#100                    wsprintf(chTemp,_T("ThreadID=%d\r\n"),m_dwThreadID);

#101                    ::OutputDebugString(chTemp);

#102

#103                     //目前沒有做什么事情,就讓線程釋放一下CPU

#104                    Sleep(10);

#105               }

#106               else if (dwRet == WAIT_OBJECT_0)

#107               {

#108                    //退出線程。

#109                    ::OutputDebugString(_T("Run() 退出線程\r\n"));

#110                    break;

#111               }

#112               else if (dwRet == WAIT_ABANDONED)

#113               {

#114                    //出錯。

#115                    ::OutputDebugString(_T("Run() 線程出錯\r\n"));

#116                    return -1;

#117               }

#118         }

#119

#120         return 0;

#121  }

#122

#123 protected:

#124  HANDLE m_hThread;         //線程句柄。

#125  DWORD m_dwThreadID;          //線程ID

#126

#127  HANDLE m_hEventExit;    //線程退出事件。

#128 };

 

上面在第35行創建線程退出事件,第95行檢查事件是否可退出線程運行,第58行設置退出線程的事件。

轉自:http://blog.163.com/bluesky_07_06_1/blog/static/1644400832010728101414986/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM