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。使用函數的這個原型生成的線程也有消息機制,此消息機制同主線程的機制幾乎一樣。

        在工作線程中使用的函數指針一般是指向全局函數的而不是類成員函數,因爲這牽扯到對象的生命週期,如果一個對象在線程執行時被銷燬了,那麼這個線程的行爲就成爲不確定的了。

(二) 線程間通訊

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

使用全局變量進行通信

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

我們可以在一個線程的執行函數中向另一個線程發送自定義的消息來達到通信的目的。一個線程向另外一個線程發送消息是通過操作系統實現的。利用 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類的用法非常簡單,步驟如下:

定義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函數來監視事件狀態。

        對於Event對象我們有兩種實現方法,一個是CEvent,這是MFC提供給我們的,另外一個就是使用CreateEvent函數,此函數的定義如下:

          HANDLE CreateEvent( 
                            LPSECURITY_ATTRIBUTES lpEventAttributes, 
                            BOOL bManualReset,
                            BOOL bInitialState, 
                            LPCTSTR lpName );
 
      此函數返回一個內核對象的句柄,在一般的情況下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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章