1. 程式人生 > >MFC 多執行緒總結

MFC 多執行緒總結

(一) 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。使用函式的這個原型生成的執行緒也有訊息機制,此訊息機制同主執行緒的機制幾乎一樣。

        在工作執行緒中使用的函式指標一般是指向全域性函式的而不是類成員函式,因為這牽扯到物件的生命週期,如果一個物件線上程執行時被銷燬了,那麼這個執行緒的行為就成為不確定的了。

(二) 執行緒間通訊

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

  1. 使用全域性變數進行通訊

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

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

               1.  在標頭檔案stdafx.h中增加一個自定義訊息巨集 
                              #define WM_USER_THREADEND WM_USER + 1

               2.  在於增加新訊息的視窗或對話方塊類的標頭檔案中增加一個回撥函式宣告,注意要宣告為public
                              afx_msg LRESULT OnUserThreadend(WPARAM wParam, LPARAM lParam);

               3.  在視窗或對話方塊的cpp檔案的BEGIN_MESSAGE_MAP,END_MESSAGE_MAP 中增加一行         
                              ON_MESSAGE(WM_USER_THREADEND, OnUserThreadend)
               
               4.  在視窗或對話方塊的cpp檔案中增加回調函式的實現,如:
                               LRESULT ThreadDialog::OnUserThreadend(WPARAM wParam, LPARAM lParam)
                                {
                                                TRACE("WM_USER_THREADEND message /n");
                                                return 0;
                                }      

               5.  自定義訊息的觸發
                               ::PostMessage(GetSafeHwnd(), WM_USER_THREADEND, 0, 0);
                     其中GetSafeHwnd()得到了一個當前視窗的控制代碼,此訊息將發給當前視窗,如果想傳送訊息給其它                         視窗只需改變這個控制代碼,前提是目的視窗也實現了此訊息的處理函式。

(三) 執行緒同步

  雖然多執行緒能給我們帶來好處,但是也有不少問題需要解決。例如,對於像磁碟驅動器這樣獨佔性系統資源,由於執行緒可以執行程序的任何程式碼段,且執行緒的運 行是由系統排程自動完成的,具有一定的不確定性,因此就有可能出現兩個執行緒同時對磁碟驅動器進行操作,從而出現操作錯誤;又例如,對於銀行系統的計算機來 說,可能使用一個執行緒來更新其使用者資料庫,而用另外一個執行緒來讀取資料庫以響應儲戶的需要,極有可能讀資料庫的執行緒讀取的是未完全更新的資料庫,因為可能 在讀的時候只有一部分資料被更新過。

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

  • 臨界區(CCriticalSection)
  • 事件(CEvent)
  • 互斥量(CMutex)
  • 訊號量(CSemaphore)
         通過這些類,我們可以比較容易地做到執行緒同步。

A、使用 CCriticalSection 類

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

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

  1. 定義CCriticalSection類的一個全域性物件(以使各個執行緒均能訪問),如CCriticalSection critical_section;
  2. 在訪問需要保護的資源或程式碼之前,呼叫CCriticalSection類的成員Lock()獲得臨界區物件:
    critical_section.Lock();
    線上程中呼叫該函式來使執行緒獲得它所請求的臨界區。如果此時沒有其它執行緒佔有臨界區物件,則呼叫Lock()的執行緒獲得臨界區;否則,執行緒將被掛起,並放入到一個系統佇列中等待,直到當前擁有臨界區的執行緒釋放了臨界區時為止。
  3. 訪問臨界區完畢後,使用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函式來監視事件狀態。

        對於Event物件我們有兩種實現方法,一個是CEvent,這是MFC提供給我們的,另外一個就是使用CreateEvent函式,此函式的定義如下:
          HANDLE CreateEvent(
                            LPSECURITY_ATTRIBUTES ,
                            BOOL ,
                            BOOL ,
                            LPCTSTR );
 
      此函式返回一個核心物件的控制代碼,在一般的情況下CEvent是第一選擇,但是在我使用CEvent的過程中WaitForMultipleObjects對CEvent物件並不能很好的工作。
      WaitForMultipleObjects函式中有一個引數型別是HANDLE*。MSDN上的說明指出此HANDLE指標並不能接收處理CEvent物件,如下:

         The WaitForMultipleObjects function can specify handles of any of the following object types in the                  lpHandles array:

      • Change notification
      • Console input
      • Event
      • Job
      • Memory resource notification
      • Mutex
      • Process
      • Semaphore
      • Thread
      • Waitable timer
          所以當我們需要使用此函式是隻能選擇使用核心物件Event。

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。