多執行緒學習筆記

Mr_John_Liang發表於2013-06-03

多執行緒學習筆記




多執行緒概述


  程式和執行緒都是作業系統的概念。程式是應用程式的執行例項,每個程式是由私有的虛擬地址空間、程式碼、資料和其它各種系統資源組成,程式在執行過程中建立的資源隨著程式的終止而被銷燬,所使用的系統資源在程式終止時被釋放或關閉。

  執行緒是程式內部的一個執行單元。系統建立好程式後,實際上就啟動執行了該程式的主執行執行緒,主執行執行緒以函式地址形式,比如說main或WinMain函式,將程式的啟動點提供給Windows系統。主執行執行緒終止了,程式也就隨之終止。

  每一個程式至少有一個主執行執行緒,它無需由使用者去主動建立,是由系統自動建立的。使用者根據需要在應用程式中建立其它執行緒,多個執行緒併發地執行於同一個程式中。一個程式中的所有執行緒都在該程式的虛擬地址空間中,共同使用這些虛擬地址空間、全域性變數和系統資源,所以執行緒間的通訊非常方便,多執行緒技術的應用也較為廣泛。

  多執行緒可以實現並行處理,避免了某項任務長時間佔用CPU時間。要說明的一點是,目前大多數的計算機都是單處理器(CPU)的,為了執行所有這些執行緒,作業系統為每個獨立執行緒安排一些CPU時間,作業系統以輪換方式向執行緒提供時間片,這就給人一種假象,好象這些執行緒都在同時執行。由此可見,如果兩個非常活躍的執行緒為了搶奪對CPU的控制權,線上程切換時會消耗很多的CPU資源,反而會降低系統的效能。這一點在多執行緒程式設計時應該注意。

  Win32 SDK函式支援進行多執行緒的程式設計,並提供了作業系統原理中的各種同步、互斥和臨界區等操作。Visual C++ 6.0中,使用MFC類庫也實現了多執行緒的程式設計,使得多執行緒程式設計更加方便。


Win32 API對多執行緒程式設計的支援


  Win32 提供了一系列的API函式來完成執行緒的建立、掛起、恢復、終結以及通訊等工作。下面將選取其中的一些重要函式進行說明。


1、HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,


DWORD dwStackSize,


LPTHREAD_START_ROUTINE lpStartAddress,


LPVOID lpParameter,


DWORD dwCreationFlags,


LPDWORD lpThreadId);


該函式在其呼叫程式的程式空間裡建立一個新的執行緒,並返回已建執行緒的控制程式碼,其中各引數說明如下:


lpThreadAttributes:指向一個 SECURITY_ATTRIBUTES 結構的指標,該結構決定了執行緒的安全屬性,一般置為 NULL;


dwStackSize:指定了執行緒的堆疊深度,一般都設定為0;


lpStartAddress:表示新執行緒開始執行時程式碼所在函式的地址,即執行緒的起始地址。一般情況為(LPTHREAD_START_ROUTINE)ThreadFunc,ThreadFunc 是執行緒函式名;


lpParameter:指定了執行緒執行時傳送給執行緒的32位引數,即執行緒函式的引數;


dwCreationFlags:控制執行緒建立的附加標誌,可以取兩種值。如果該引數為0,執行緒在被建立後就會立即開始執行;如果該引數為CREATE_SUSPENDED,則系統產生執行緒後,該執行緒處於掛起狀態,並不馬上執行,直至函式ResumeThread被呼叫;


lpThreadId:該引數返回所建立執行緒的ID;


如果建立成功則返回執行緒的控制程式碼,否則返回NULL。


2、DWORD SuspendThread(HANDLE hThread);


該函式用於掛起指定的執行緒,如果函式執行成功,則執行緒的執行被終止。


3、DWORD ResumeThread(HANDLE hThread);


該函式用於結束執行緒的掛起狀態,執行執行緒。


4、VOID ExitThread(DWORD dwExitCode);


該函式用於執行緒終結自身的執行,主要線上程的執行函式中被呼叫。其中引數dwExitCode用來設定執行緒的退出碼。


5、BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);


  一般情況下,執行緒執行結束之後,執行緒函式正常返回,但是應用程式可以呼叫TerminateThread強行終止某一執行緒的執行。各引數含義如下:


hThread:將被終結的執行緒的控制程式碼;


dwExitCode:用於指定執行緒的退出碼。


使用TerminateThread()終止某個執行緒的執行是不安全的,可能會引起系統不穩定;雖然該函式立即終止執行緒的執行,但並不釋放執行緒所佔用的資源。因此,一般不建議使用該函式。


6、 BOOL GetExitCodeThread(


HANDLE hThread, // handle to the thread


LPDWORD lpExitCode // address to receive termination status


);


得到終止執行緒狀態,如果狀態為STILL_ACTIVE,執行緒沒有終止,否則執行緒終止。


7、BOOL PostThreadMessage(DWORD idThread,


UINT Msg,


WPARAM wParam,


LPARAM lParam);


該函式將一條訊息放入到指定執行緒的訊息佇列中,並且不等到訊息被該執行緒處理時便返回。


idThread:將接收訊息的執行緒的ID;


Msg:指定用來傳送的訊息;


wParam:同訊息有關的字引數;


lParam:同訊息有關的長引數;


呼叫該函式時,如果即將接收訊息的執行緒沒有建立訊息迴圈,則該函式執行失敗。


注:沒有對應SendThreadMessage函式,因為SendMessage是不安全的,傳送訊息到一個視窗,自己等待,訊息處理完成之後返回。如果訊息始終沒有處理完成返回的話,就會存在死鎖問題,所以執行緒中沒有對應SendThreadMessage之類的函式。


SendMessag、PostMessage、GetMessage、PeekMessage區別


SendMessag是傳送訊息到另一個視窗,自己等待,訊息處理完成之後返回。(表面上另一個視窗訊息處理是自己視窗來執行完成的,其實另一個視窗訊息處理真正的執行者是SendMessag這個視窗)


PostMessage是傳送訊息到訊息佇列中,自己馬上返回。


GetMessage訊息過濾,等到有合適的訊息時才返回,同時會將訊息從佇列中刪除。


PeekMessage訊息過濾,檢視了一下訊息佇列,PeekMessage可以設定最後一個引數wRemoveMsg來決定是否將訊息保留在佇列中。


MFC對多執行緒程式設計的支援


  MFC中有兩類執行緒,分別稱之為工作者執行緒和使用者介面執行緒。二者的主要區別在於工作者執行緒沒有訊息迴圈,而使用者介面執行緒有自己的訊息佇列和訊息迴圈。

  工作者執行緒沒有訊息機制,通常用來執行後臺計算和維護任務,如冗長的計算過程,印表機的後臺列印等。使用者介面執行緒一般用於處理獨立於其他執行緒執行之外的使用者輸入,響應使用者及系統所產生的事件和訊息等。但對於Win32的API程式設計而言,這兩種執行緒是沒有區別的,它們都只需執行緒的啟動地址即可啟動執行緒來執行任務。

  在MFC中,一般用全域性函式AfxBeginThread()來建立並初始化一個執行緒的執行,該函式有兩種過載形式,分別用於建立工作者執行緒和使用者介面執行緒。兩種過載函式原型和引數分別說明如下:


(1) CWinThread* AfxBeginThread(AFX_THREADPROC pfnThreadProc,


LPVOID pParam,


nPriority=THREAD_PRIORITY_NORMAL,


UINT nStackSize=0,


DWORD dwCreateFlags=0,


LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL);


PfnThreadProc:指向工作者執行緒的執行函式的指標,執行緒函式原型必須宣告如下:


UINT ExecutingFunction(LPVOID pParam);


請注意,ExecutingFunction()應返回一個UINT型別的值,用以指明該函式結束的原因。一般情況下,返回0表明執行成功。


pParam:傳遞給執行緒函式的一個32位引數,執行函式將用某種方式解釋該值。它可以是數值,或是指向一個結構的指標,甚至可以被忽略;


nPriority:執行緒的優先順序。如果為0,則執行緒與其父執行緒具有相同的優先順序;


nStackSize:執行緒為自己分配堆疊的大小,其單位為位元組。如果nStackSize被設為0,則執行緒的堆疊被設定成與父執行緒堆疊相同大小;


dwCreateFlags:如果為0,則執行緒在建立後立刻開始執行。如果為CREATE_SUSPEND,則執行緒在建立後立刻被掛起;


lpSecurityAttrs:執行緒的安全屬性指標,一般為NULL;


(2) CWinThread* AfxBeginThread(CRuntimeClass* pThreadClass,


int nPriority=THREAD_PRIORITY_NORMAL,


UINT nStackSize=0,


DWORD dwCreateFlags=0,


LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL);


pThreadClass 是指向 CWinThread 的一個匯出類的執行時類物件的指標,該匯出類定義了被建立的使用者介面執行緒的啟動、退出等;其它引數的意義同形式1。使用函式的這個原型生成的執行緒也有訊息機制,在以後的例子中我們將發現同主執行緒的機制幾乎一樣。


下面我們對CWinThread類的資料成員及常用函式進行簡要說明。


m_hThread:當前執行緒的控制程式碼;


m_nThreadID:當前執行緒的ID;


m_pMainWnd:指向應用程式主視窗的指標.


執行緒間通訊


  一般而言,應用程式中的一個次要執行緒總是為主執行緒執行特定的任務,這樣,主執行緒和次要執行緒間必定有一個資訊傳遞的渠道,也就是主執行緒和次要執行緒間要進行通訊。這種執行緒間的通訊不但是難以避免的,而且在多執行緒程式設計中也是複雜和頻繁的,下面將進行說明。


使用全域性變數進行通訊


由於屬於同一個程式的各個執行緒共享作業系統分配該程式的資源,故解決執行緒間通訊最簡單的一種方法是使用全域性變數。對於標準型別的全域性變數,我們建議使用volatile 修飾符,它告訴編譯器無需對該變數作任何的優化,即無需將它放到一個暫存器中,並且該值可被外部改變。如果執行緒間所需傳遞的資訊較複雜,我們可以定義一個結構,通過傳遞指向該結構的指標進行傳遞資訊。

 


使用自定義訊息


我們可以在一個執行緒的執行函式中向另一個執行緒傳送自定義的訊息來達到通訊的目的。一個執行緒向另外一個執行緒傳送訊息是通過作業系統實現的。利用Windows作業系統的訊息驅動機制,當一個執行緒發出一條訊息時,作業系統首先接收到該訊息,然後把該訊息轉發給目標執行緒,接收訊息的執行緒必須已經建立了訊息迴圈。


執行緒的同步


  雖然多執行緒能給我們帶來好處,但是也有不少問題需要解決。例如,對於像磁碟驅動器這樣獨佔性系統資源,由於執行緒可以執行程式的任何程式碼段,且執行緒的執行是由系統排程自動完成的,具有一定的不確定性,因此就有可能出現兩個執行緒同時對磁碟驅動器進行操作,從而出現操作錯誤。

使隸屬於同一程式的各執行緒協調一致地工作稱為執行緒的同步。MFC提供了多種同步物件,下面我們只介紹最常用的四種:


臨界區(CCriticalSection)


事件(CEvent)


互斥量(CMutex)


訊號量(CSemaphore)


A、使用 CCriticalSection 類


  當多個執行緒訪問一個獨佔性共享資源時,可以使用“臨界區”物件。任一時刻只有一個執行緒可以擁有臨界區物件,擁有臨界區的執行緒可以訪問被保護起來的資源或程式碼段,其他希望進入臨界區的執行緒將被掛起等待,直到擁有臨界區的執行緒放棄臨界區時為止,這樣就保證了不會在同一時刻出現多個執行緒訪問共享資源。


CCriticalSection類的用法非常簡單,步驟如下:

 


定義CCriticalSection類的一個全域性物件(以使各個執行緒均能訪問),如CCriticalSection critical_section;


在訪問需要保護的資源或程式碼之前,呼叫CCriticalSection類的成員Lock()獲得臨界區物件:


critical_section.Lock();


線上程中呼叫該函式來使執行緒獲得它所請求的臨界區。如果此時沒有其它執行緒佔有臨界區物件,則呼叫Lock()的執行緒獲得臨界區;否則,執行緒將被掛起,並放入到一個系統佇列中等待,直到當前擁有臨界區的執行緒釋放了臨界區時為止。


訪問臨界區完畢後,使用CCriticalSection的成員函式Unlock()來釋放臨界區:


critical_section.Unlock();


再通俗一點講,就是執行緒A執行到critical_section.Lock();語句時,如果其它執行緒(B)正在執行critical_section.Lock();語句後且critical_section. Unlock();語句前的語句時,執行緒A就會等待,直到執行緒B執行完critical_section. Unlock();語句,執行緒A才會繼續執行。


B、使用 CEvent 類


  CEvent 類提供了對事件的支援。事件是一個允許一個執行緒在某種情況發生時,喚醒另外一個執行緒的同步物件。例如在某些網路應用程式中,一個執行緒(記為A)負責監聽通訊埠,另外一個執行緒(記為B)負責更新使用者資料。通過使用CEvent 類,執行緒A可以通知執行緒B何時更新使用者資料。每一個CEvent 物件可以有兩種狀態:有訊號狀態和無訊號狀態。執行緒監視位於其中的CEvent 類物件的狀態,並在相應的時候採取相應的操作。

  在MFC中,CEvent 類物件有兩種型別:人工事件和自動事件。一個自動CEvent 物件在被至少一個執行緒釋放後會自動返回到無訊號狀態;而人工事件物件獲得訊號後,釋放可利用執行緒,但直到呼叫成員函式ReSetEvent()才將其設定為無訊號狀態。在建立CEvent 類的物件時,預設建立的是自動事件。 CEvent 類的各成員函式的原型和引數說明如下:


1、CEvent(BOOL bInitiallyOwn=FALSE,


BOOL bManualReset=FALSE,


LPCTSTR lpszName=NULL,


LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);


bInitiallyOwn:指定事件物件初始化狀態,TRUE為有訊號,FALSE為無訊號;


bManualReset:指定要建立的事件是屬於人工事件還是自動事件。TRUE為人工事件,FALSE為自動事件;


後兩個引數一般設為NULL,在此不作過多說明。


2、BOOL CEvent::SetEvent();


  將 CEvent 類物件的狀態設定為有訊號狀態。如果事件是人工事件,則 CEvent 類物件保持為有訊號狀態,直到呼叫成員函式ResetEvent()將 其重新設為無訊號狀態時為止。如果CEvent 類物件為自動事件,則在SetEvent()將事件設定為有訊號狀態後,CEvent 類物件由系統自動重置為無訊號狀態。


如果該函式執行成功,則返回非零值,否則返回零。


3、BOOL CEvent::ResetEvent();


該函式將事件的狀態設定為無訊號狀態,並保持該狀態直至SetEvent()被呼叫時為止。由於自動事件是由系統自動重置,故自動事件不需要呼叫該函式。如果該函式執行成功,返回非零值,否則返回零。我們一般通過呼叫WaitForSingleObject函式來監視事件狀態。


C、使用CMutex 類


  互斥物件與臨界區物件很像.互斥物件與臨界區物件的不同在於:互斥物件可以在程式間使用,而臨界區物件只能在同一程式的各執行緒間使用。當然,互斥物件也可以用於同一程式的各個執行緒間,但是在這種情況下,使用臨界區會更節省系統資源,更有效率。


D、使用CSemaphore 類


  當需要一個計數器來限制可以使用某個執行緒的數目時,可以使用“訊號量”物件。CSemaphore 類的物件儲存了對當前訪問某一指定資源的執行緒的計數值,該計數值是當前還可以使用該資源的執行緒的數目。如果這個計數達到了零,則所有對這個CSemaphore 類物件所控制的資源的訪問嘗試都被放入到一個佇列中等待,直到超時或計數值不為零時為止。一個執行緒被釋放已訪問了被保護的資源時,計數值減1;一個執行緒完成了對被控共享資源的訪問時,計數值增1。這個被CSemaphore 類物件所控制的資源可以同時接受訪問的最大執行緒數在該物件的構建函式中指定。


CSemaphore 類的建構函式原型及引數說明如下:


CSemaphore (LONG lInitialCount=1,


LONG lMaxCount=1,


LPCTSTR pstrName=NULL,


LPSECURITY_ATTRIBUTES lpsaAttributes=NULL);


lInitialCount:訊號量物件的初始計數值,即可訪問執行緒數目的初始值;


lMaxCount:訊號量物件計數值的最大值,該引數決定了同一時刻可訪問由訊號量保護的資源的執行緒最大數目;


後兩個引數在同一程式中使用一般為NULL,不作過多討論;


  在用CSemaphore 類的建構函式建立訊號量物件時要同時指出允許的最大資源計數和當前可用資源計數。一般是將當前可用資源計數設定為最大資源計數,每增加一個執行緒對共享資源的訪問,當前可用資源計數就會減1,只要當前可用資源計數是大於0的,就可以發出訊號量訊號。但是當前可用計數減小到0時,則說明當前佔用資源的執行緒數已經達到了所允許的最大數目,不能再允許其它執行緒的進入,此時的訊號量訊號將無法發出。執行緒在處理完共享資源後,應在離開的同時通過ReleaseSemaphore()函式將當前可用資源數加1。


互斥物件、臨界區、事件、訊號量之間的區別:


互斥物件與臨界區物件很像.互斥物件與臨界區物件的不同在於:互斥物件可以在程式間使用,而臨界區物件只能在同一程式的各執行緒間使用。命名的互斥物件可以在程式間使用.


事件是一個允許一個執行緒在某種情況發生時,喚醒另外一個執行緒的同步物件。


“訊號量”物件通過一個計數器來限制可以使用某個執行緒的數目。計數達到了零時,執行緒進入等待佇列中等待。計數大於零時,執行緒可以訪問資源,同時計數減一。


程式設計中注意細節


1、volatile 修飾符的作用是告訴編譯器無需對該變數作任何的優化,即無需將它放到一個暫存器中,並且該值可被外部改變。對於多執行緒引用的全域性變數來說,volatile 是一個非常重要的修飾符。

2、WaitForSingleObject


DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);


hHandle為要監視的物件(一般為同步物件,也可以是執行緒)的控制程式碼;


dwMilliseconds為hHandle物件所設定的超時值,單位為毫秒;


當在某一執行緒中呼叫該函式時,執行緒暫時掛起,系統監視hHandle所指向的物件的狀態。如果在掛起的dwMilliseconds毫秒內,執行緒所等待的物件變為有訊號狀態,則該函式立即返回;如果超時時間已經到達dwMilliseconds毫秒,但hHandle所指向的物件還沒有變成有訊號狀態,函式照樣返回。引數dwMilliseconds有兩個具有特殊意義的值:0和INFINITE。若為0,則該函式立即返回;若為INFINITE,則執行緒一直被掛起,直到hHandle所指向的物件變為有訊號狀態時為止。


3、使用CreateThread出現類似cannot convert parameter 3 from 'unsigned int (void *)' to 'unsigned long (__stdcall *)(void *)'中文,需要將引數3強制轉換成LPTHREAD_START_ROUTINE。


4、CreateThread


執行緒函式引數型別為:LPTHREAD_START_ROUTINE


定義:ypedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(


LPVOID lpThreadParameter


);


AfxBeginThread


執行緒函式引數型別為:AFX_THREADPROC


定義:typedef UINT (AFX_CDECL *AFX_THREADPROC)(LPVOID);


5、ON_THREAD_MESSAGE 表示執行緒訊息對映。


6、儘量少的使用全域性變數、static變數做共享資料,儘量使用引數傳遞物件。被引數傳遞的物件,應該只包括必需的成員變數。所謂必需的成員變數,就是必定會被多執行緒操作的。


7、在MFC中請慎用執行緒。因為MFC的框架假定你的訊息處理都是在主執行緒中完成的。首先視窗控制程式碼是屬於執行緒的,如果擁有視窗控制程式碼的執行緒退出了,如果另一個執行緒處理這個視窗控制程式碼,系統就會出現問題。而MFC為了避免這種情況的發生,使你在子執行緒中呼叫訊息(視窗)處理函式時,就會不停的出Assert錯誤,煩都煩死你。典型的例子就時CSocket,因為CSocket是使用了一個隱藏視窗實現了假阻塞,所以不可避免的使用了訊息處理函式,如果你在子執行緒中使用CSocket,你就可能看到assert的彈出了。


8、不要在不同的執行緒中同時註冊COM元件。兩個執行緒,一個註冊1.ocx, 2.ocx, 3.ocx, 4.ocx; 而另一個則註冊5.ocx, 6.ocx, 7.ocx, 8.ocx,結果死鎖發生了,分別死在FreeLibrary和DllRegisterServer,因為這8個ocx是用MFC中做的,也可能是MFC的Bug,但DllRegisterServer卻死在GetModuleFileName裡。


9、不要把執行緒搞的那麼複雜。很多初學者,恨不能用上執行緒相關的所有的函式,這裡互斥,那裡等待,一會兒起執行緒,一會兒關執行緒的。好的多執行緒程式,應該是儘量少的使用執行緒。這句話怎麼理解吶,就是說盡量統一一塊資料共享區存放資料佇列,工作子執行緒從佇列中取資料,處理,再放回資料,這樣才會模組化,物件化;而不是每個資料都起一個工作子執行緒處理,處理完了就關閉,寫的時候雖然直接,等維護起來就累了。


常用執行緒問題


1、線上程裡用控制元件是不明智的選擇。


2、多執行緒的自動啟動方法.


A、視窗建立後,執行AfxBeginThread.但終止執行緒時,比較麻煩。有時你還必須用CloseHandle和TerminateThread來強行終止執行緒。這樣容易造成記憶體洩露。


B、設定一個CEvent類物件,你可以控制他的訊號量(分兩種:被觸發,未被觸發),在建立執行緒時,設定執行緒掛起並等待訊號。這樣,線上程建立後(你可以提早建立執行緒,但它時被掛起的),你就可以想什麼時候啟動執行緒就啟動執行緒。而且關閉也很方便(事件觸發)。這是微軟推薦做法。

3、不要跨執行緒訪問複雜的MFC物件。大多數複雜的MFC物件的內部實現引用了執行緒區域性儲存(TLS)。線上程中傳送一個自定義訊息到視窗控制程式碼就可以訪問了。


4、mfc的大多數類不是執行緒安全的,cwnd及其訊息路由是其中之最。mfc介面類的大多數方法,最後都是通過sendmessage實現的,而訊息處理的過程中會引發其他訊息的傳送及處理。如果訊息處理函式本身不是執行緒安全的。你從工作執行緒中呼叫這些方法遲早會同你介面執行緒的使用者訊息響應發生衝突。


5、Cxxxx::fromhandle會根據呼叫者所線上程查表,如果查不到使用者建立的Cxxxx對應物件,它會建立一個臨時物件出來並返回給你,你根本不可能期望它的成員變數會是有意義的。所以要用也只能用cwnd::fromhandle,因為它只包含一個m_hwnd成員。不過,要記住跨執行緒直接或間接地呼叫::sendmessage,通常都是行為不可預測的。


6、一個執行緒不可以也不應該訪問另一個執行緒中的包裝類物件(因為包裝類物件就相當於視窗,這是MFC的目標,並不是包裝類本身不能被跨執行緒訪問),“不可以”就是通過在包裝類成員函式中的斷言巨集實現的(在CWnd::AssertValid中),而“不應該”下面會解釋。

雖然包裝類物件不能跨執行緒訪問,但是視窗控制程式碼卻可以跨執行緒訪問。因為包裝類物件不僅等同於視窗,還改變了視窗的互動方式(這也正是C++類的概念的應用),使得不用非得使用訊息機制才能和視窗互動。注意前面提到的,如果跨執行緒訪問包裝類物件,而又使用C++類的概念操作它,則其必須進行執行緒保護,而“不能跨執行緒訪問”就消除了這個問題。因此臨時物件的產生就只是如前面所說,方便程式碼的編寫而已,不提供子類化的效果,因為視窗控制程式碼可以跨執行緒訪問。


視窗類


視窗類是一個結構,其一個例項代表著一個視窗型別,與C++中的類的概念非常相近(雖然其表現形式完全不同,C++的類只不過是記憶體佈局和其上的操作這個概念的型別),故被稱作為視窗類。

視窗是具有裝置操作能力的邏輯概念,即一種能操作裝置(通常是顯示器)的東西。由於視窗是視窗類的例項,就象C++中的一個類的例項,是可以具有成員函式的(雖然表現形式不同),但一定要明確視窗的目的--操作裝置(這點也可以從Microsoft針對視窗所制訂的API的功能看出,主要出於對裝置操作的方便)。因此不應因為其具有成員函式的功能而將視窗用於功能物件的建立,這雖然不錯,但是嚴重違反了語義的需要,是不提倡的,但卻由於MFC介面包裝類的加入導致大多數程式設計師經常將邏輯混入介面。

視窗類是個結構,其中的大部分成員都沒什麼重要意義,只是Microsoft一相情願制訂的,如果不想使用介面API(Windows User Interface API),可以不管那些成員。其中只有一個成員是重要的--lpfnWndProc,訊息處理函式。

外界(使用視窗的程式碼)只能通過訊息操作視窗,這就如同C++中編寫的具有良好的物件導向風格的類的例項只能通過其公共成員函式對其進行操作。因此訊息處理函式就代表了一個視窗的一切(忽略視窗類中其他成員的作用)。很容易發現,視窗這個例項只具有成員函式(訊息處理函式),不具有成員變數,即沒有一塊特定記憶體和一特定的視窗相關聯,則視窗將不能具有狀態(Windows還是提供了Window Properties API來緩和這種狀況)。這也正是上面問題發生的根源。

為了處理視窗不能具有狀態的問題(這其實正是Windows靈活的表現),可以有很多種方法,而MFC出於能夠很容易的對已有視窗類進行擴充套件,選擇了使用一個對映將一個視窗控制程式碼(視窗的唯一標示符)和一個記憶體塊進行繫結,而這塊記憶體塊就是我們熟知的MFC介面包裝類(從CWnd開始派生延續)的例項。


MFC狀態


狀態就是例項通過某種手段使得資訊可以跨時間段重現,C++的類的例項就是由外界通過公共成員函式改變例項的成員變數的值以實現具有狀態的效果。在MFC 中,具有三種狀態:模組狀態、程式狀態、執行緒狀態。分別為模組、程式和執行緒這三種例項的狀態。由於程式碼是由執行緒執行,且和另外兩個的關係也很密切,因此也被稱作本地資料。

模組本地資料

具有模組本地性的變數。模組指一個載入到程式虛擬記憶體空間中的PE檔案,即exe檔案本身和其載入的dll檔案。而模組本地性即同樣的指標,根據程式碼從不同的模組執行而訪問不同的記憶體空間。這其實只用每個模組都宣告一個全域性變數,通過一個切換的過程即可實現模組本地性。MFC中,這個過程是通過呼叫AfxSetModuleState來切換的,而通常都使用 AFX_MANAGE_STATE這個巨集來處理,因此下面常見的語句就是用於模組狀態的切換的:

AFX_MANAGE_STATE( AfxGetStaticModuleState() );

MFC中定義了一個結構(AFX_MODULE_STATE),其例項具有模組本地性,記錄了此模組的全域性應用程式物件指標、資源控制程式碼等模組級的全域性變數。其中有一個成員變數是執行緒本地資料,型別為AFX_MODULE_THREAD_STATE,其就是本文問題的關鍵。

程式本地資料

具有程式本地性的變數。與模組本地性相同,即同一個指標,在不同程式中指向不同的記憶體空間。這一點Windows本身的虛擬記憶體空間這個機制已經實現了,不過在dll中定義的全域性變數,如果dll支援Win32s,則其是共享其全域性變數的,即不同的程式載入了同一dll將訪問同一記憶體。Win32s是為了那些基於Win32的應用程式能在Windows 3.1上執行,由於Windows 3.1是16位作業系統,早已被淘汰,而現行的dll模型其本身就已經實現了程式本地性(不過還是可以通過共享節來實現Win32s中的dll的效果),因此程式狀態其實就是一全域性變數。

MFC中作為本地資料的結構有很多,如_AFX_WIN_STATE、_AFX_DEBUG_STATE、_AFX_DB_STATE等,都是MFC內部自己使用的具有程式本地性的全域性變數。

執行緒本地資料

具有執行緒本地性的變數。如上,即同一個指標,不同的執行緒將會訪問不同的記憶體空間。這點MFC是通過執行緒本地儲存(TLS--Thread Local Storage,其使用方法由於與本文無關,在此不表)實現的。

MFC中定義了一個結構(_AFX_THREAD_STATE)以記錄某些執行緒級的全域性變數,如最近一次的模組狀態指標,最近一次的訊息等。

模組執行緒狀態

MFC中定義的一個結構(AFX_MODULE_THREAD_STATE),其例項即具有執行緒本地性又具有模組本地性。也就是說不同的執行緒從同一模組中和同一執行緒從不同模組中訪問MFC庫函式都將導致操作不同的記憶體空間。其應用在AFX_MODULE_STATE中,記錄一些執行緒相關但又模組級的資料,如本文的重點--視窗控制程式碼對映。


包裝類物件和控制程式碼對映


控制程式碼對映--CHandleMap,MFC提供的一個底層輔助類,程式設計師是不應該直接使用它的。其有兩個重要的成員變數:CMapPtrToPtr m_permanentMap, m_temporaryMap;。分別記錄永久控制程式碼繫結和臨時控制程式碼繫結。前面說過,MFC使用一個對映將視窗控制程式碼和其包裝類的例項繫結在一起,m_permanentMap和m_temporaryMap就是這個對映,對映分為永久包裝類物件和臨時包裝類物件,而在前面提到過的 AFX_MODULE_THREAD_STATE中就有一個成員變數:CHandleMap* m_pmapHWND;(之所以是CHandleMap*是使用懶惰程式設計法,儘量節約資源)以專門完成HWND的繫結對映,除此以外還有如 m_pmapHDC、m_pmapHMENU等成員變數以分別實現HDC、HMENU的綁頂對映。而為什麼這些對映要放在模組執行緒狀態而不放線上程狀態或模組狀態是很明顯的--這些包裝類包裝的控制程式碼都是和執行緒相關的(如HWND只有建立它的執行緒才能接收其訊息)且這個模組中的包裝類物件可能不同於另一個模組的(如包裝類是某個DLL中專門派生的一個類,如a.dll中定義的CAButton的例項和b.dll中定義的CBButton的例項如果同時在一個執行緒中。此時執行緒解除安裝了a.dll,然後CAButton的例項得到訊息並進行處理,將發生嚴重錯誤--類程式碼已經被解除安裝掉了)。


包裝類存在的意義有二:包裝對HWND的操作以加速程式碼的編寫和提供視窗子類化(不是超類化)的效果以派生視窗類。包裝類物件針對執行緒分為兩種:永久包裝類物件(以後簡稱永久物件)和臨時包裝類物件(以後簡稱臨時物件)。臨時物件的意義僅僅只有包裝對HWND的操作以加速程式碼編寫,不具有派生視窗類的功能。永久物件則具有前面說的包裝類的兩個意義。

在建立視窗時(即CWnd::CreateEx中),MFC通過鉤子提前(WM_CREATE和WM_NCCREATE之前)處理了通知,用AfxWndProc子類化了建立的視窗並將對應的CWnd*加入當前執行緒的永久物件的對映中,而在AfxWndProc中,總是由CWnd::FromHandlePermanent(獲得對應HWND的永久物件)得到當前執行緒中當前訊息所屬視窗控制程式碼對應的永久物件,然後通過呼叫得到的CWnd*的WindowProc成員函式來處理訊息以實現派生視窗類的效果。這也就是說永久物件具有視窗子類化的意義,而不僅僅是封裝HWND的操作。

要將一個HWND和一個已有的包裝類物件相關聯,呼叫CWnd::Attach將此包裝類物件和HWND對映成永久物件(但這種方法得到的永久物件不一定具有子類化功能,很可能仍和臨時物件一樣,僅僅起封裝的目的)。如果想得到臨時物件,則通過CWnd::FromHandle這個靜態成員函式以獲得。臨時物件之所以叫臨時,就是其是由MFC內部(CHandleMap::FromHandle)生成,其內部(CHandleMap::DeleteTemp)銷燬(一般通過CWinThread::OnIdle中呼叫AfxUnlockTempMaps)。因此程式設計師是永遠不應該試圖銷燬臨時物件的(即使臨時物件所屬執行緒沒有訊息迴圈,不能呼叫CwinThread::OnIdle,線上程結束時,CHandleMap的析構仍然會銷燬臨時物件)。


7、MFC物件不要跨執行緒使用,因為MFC不是執行緒安全的。比如CWnd物件不要跨執行緒使用,可以用視窗控制程式碼(HWND)代替。CSocket/CAsyncSocket物件不要跨執行緒使用,用SOCKET控制程式碼代替.那麼到底什麼是執行緒安全呢?什麼時候需要考慮?如果程式涉及到多執行緒的話,就應該考慮執行緒安全問題。比如說設計的介面,將來需要在多執行緒環境中使用,或者需要跨執行緒使用某個物件時,這個就必須考慮了。所提供的介面對於執行緒來說是原子操作或者多個執行緒之間的切換不會導致該介面的執行結果存在二義性,也就是說我們不用考慮同步的問題。


一般而言“執行緒安全”由多執行緒對共享資源的訪問引起。如果呼叫某個介面時需要我們自己採取同步措施來保護該介面訪問的共享資源,則這樣的介面不是執行緒安全的.MFC和STL都不是執行緒安全的. 怎樣才能設計出執行緒安全的類或者介面呢?如果介面中訪問的資料都屬於私有資料,那麼這樣的介面是執行緒安全的.或者幾個介面對共享資料都是隻讀操作,那麼這樣的介面也是執行緒安全的.如果多個介面之間有共享資料,而且有讀有寫的話,如果設計者自己採取了同步措施,呼叫者不需要考慮資料同步問題,則這樣的介面是執行緒安全的,否則不是執行緒安全的。


例項:


DWORD WINAPI ThreadProc( void *pData ) // 執行緒函式(比如用於從COM口獲取資料)

{

// 資料獲取迴圈

// 資料獲得後放在變數i中

CAbcDialog *pDialog = reinterpret_cast( pData );

ASSERT( pDialog ); // 此處如果ASSERT_VALID( pDialog )將斷言失敗

pDialog->m_Data = i;

pDialog->UpdateData( FALSE ); // UpdateData內部ASSERT_VALID( this )斷言失敗



}

BOOL CAbcDialog::OnInitDialog()

{

CDialog::OnInitDialog();

// 其他初始化程式碼

CreateThread( NULL, 0, ThreadProc, this, 0, NULL ); // 建立執行緒

return TRUE;

}


//解決方法


#define AM_DATANOTIFY ( WM_USER + 1 )

static DWORD g_Data = 0;

DWORD WINAPI ThreadProc( void *pData ) // 執行緒函式(比如用於從COM口獲取資料)


{

// 資料獲取迴圈

// 資料獲得後放在變數i中

g_Data = i;

CWnd *pWnd = CWnd::FromHandle( reinterpret_cast( pData ) );

ASSERT_VALID( pWnd ); // 本例應該直接呼叫平臺SendMessage而不呼叫包裝類的,這裡只是演示

pWnd->SendMessage( AM_DATANOTIFY, 0, 0 );



}

BEGIN_MESSAGE_MAP( CAbcDialog, CDialog )



ON_MESSAGE( AM_DATANOTIFY, OnDataNotify )



END_MESSAGE_MAP()

BOOL CAbcDialog::OnInitDialog()

{

CDialog::OnInitDialog();

// 其他初始化程式碼

CreateThread( NULL, 0, ThreadProc, m_hWnd, 0, NULL ); // 建立執行緒

return TRUE;

}

LRESULT CAbcDialog::OnDataNotify( WPARAM /* wParam */, LPARAM /* lParam */ )

{

UpdateData( FALSE );

return 0;

}

void CAbcDialog::DoDataExchange( CDataExchange *pDX )

{

CDialog::DoDataExchange( pDX );

DDX_Text( pDX, IDC_EDIT1, g_Data );

}


8、一個主執行緒Create一個子執行緒,那麼為了保證安全退出,應該在退出時怎麼樣處理?


問題的難點在於怎麼樣知道子執行緒是否退出了。


解答:


檢索執行緒的退出程式碼


若要獲取輔助執行緒或使用者介面執行緒的退出程式碼,請呼叫 GetExitCodeThread 函式。有關此函式的資訊,請參見 Platform SDK。此函式獲取執行緒(儲存在 CWinThread 物件的 m_hThread 資料成員中)的控制程式碼和 DWORD 的地址。


如果執行緒仍然是活動的,GetExitCodeThread 會將 STILL_ACTIVE 放在提供的 DWORD 地址中;否則將退出程式碼放在此地址中。


檢索 CWinThread 物件的退出程式碼還需要一步。預設情況下,當 CWinThread 執行緒終止時,刪除該執行緒物件。這意味著不能訪問 m_hThread 資料成員,因為 CWinThread 物件不再存在。若要避免此情況,請執行以下兩個操作之一:


將 m_bAutoDelete 資料成員設定為 FALSE。這使 CWinThread 物件線上程終止後仍可以繼續存在。然後可以線上程終止後,訪問 m_hThread 資料成員。但是如果使用此技術,您有責任銷燬 CWinThread 物件,因為框架不會自動為您刪除該物件。這是首選方法。


- 或 -


單獨儲存執行緒的控制程式碼。建立執行緒後,(使用 ::DuplicateHandle)將其 m_hThread 資料成員複製到其他變數,並通過該變數訪問該成員。這樣,終止後即可以自動刪除物件,並且仍然可以查出執行緒終止的原因。請注意:在可以複製控制程式碼之前,執行緒不終止。執行此操作的最安全的方式是將 CREATE_SUSPENDED 傳遞到 AfxBeginThread,儲存控制程式碼,然後通過呼叫 ResumeThread 繼續執行執行緒。


任一方法都可以使您確定 CWinThread 物件終止的原因。


下面給出一段程式碼:


// 啟動工作者執行緒,執行緒物件不自動退出,需要手動delete


void VideoInstance::StartThreads()


{


m_pThread_MduHeart = AfxBeginThread(Thread_MDUHeart, (void*)this, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED, NULL);


ASSERT( m_pThread_MduHeart != NULL );


m_pThread_MduHeart->m_bAutoDelete = FALSE; // 這點很重要.保證執行緒退出碼在外能被檢查到。


m_pThread_MduHeart->ResumeThread();


Sleep(100);


m_pThread_RecvData = AfxBeginThread(Thread_MduRealVideo, (void*)this, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED, NULL);


ASSERT( m_pThread_RecvData != NULL );


m_pThread_RecvData->m_bAutoDelete = FALSE; // 這點很重要.保證執行緒退出碼在外能被檢查到。


m_pThread_RecvData->ResumeThread();


}


// 退出例項的執行緒物件並刪除執行緒物件,主要是為了正常退出執行緒


void VideoInstance::QuitInstance()


{


// 設定執行緒退出訊號,需要手動重置


::SetEvent(this->m_hEventQuit);


DWORD dwExitCode1 = STILL_ACTIVE;


DWORD dwExitCode2 = STILL_ACTIVE;


while(1)


{


// 檢索執行緒的退出程式碼前要求執行緒物件還沒有退出


::GetExitCodeThread(m_pThread_MduHeart->m_hThread, &dwExitCode1);


::GetExitCodeThread(m_pThread_RecvData->m_hThread, &dwExitCode2);


if( dwExitCode1 != STILL_ACTIVE && dwExitCode2 != STILL_ACTIVE)


break;


}


// 手動刪除執行緒物件


delete m_pThread_MduHeart;


delete m_pThread_RecvData;


m_pThread_MduHeart = NULL;


m_pThread_RecvData = NULL;


}

相關文章