1. 程式人生 > >C++多執行緒同步技術(MFC)

C++多執行緒同步技術(MFC)

1. 何時使用同步類

    MFC 提供的多執行緒類分為兩類:同步物件(CSyncObject、CSemaphore、CMutex、CCriticalSection 和 CEvent)和同步訪問物件(CMultiLock 和 CSingleLock)。
當必須控制對資源的訪問以確保資源的完整性時,使用同步類。同步訪問類用於獲取對這些資源的訪問權。

    若要確定應使用的同步類,請詢問以下一系列問題:

    1)應用程式必須等到發生某事才能訪問資源(例如,在將資料寫入檔案之前,必須先從通訊埠接收它)嗎?
如果是,請使用 CEvent
    2)同一應用程式內一個以上的執行緒可以同時訪問此資源(例如,應用程式允許在同一文件上最多同時開啟五個帶有檢視的視窗)嗎?
如果是,請使用 CSemaphore


    3)可以有一個以上的應用程式使用此資源(例如,資源在 DLL 中)嗎?
如果是,請使用 CMutex
如果不是,請使用 CCriticalSection
    從不直接使用 CSyncObject。它是其他四個同步類的基類。

2. 何時使用同步訪問類

    如果應用程式只與訪問單個受控資源有關,請使用 CSingleLock
    如果需要訪問多個受控資源中的任何一個,則使用 CMultiLock

3. 如何使用同步類

    寫入多執行緒應用程式時,執行緒間的同步資源訪問是一個常見問題。兩個或多個執行緒同時訪問同一資料會導致不合需要的、不可預知的結果。例如,一個執行緒可能正在更新結構的內容,而另一個執行緒正在讀取同一結構的內容。無法得知讀取執行緒將會收到何種資料:舊資料、新寫入的資料或兩種資料都有。MFC 提供了多個同步類和同步訪問類以幫助解決此問題。
    典型的多執行緒應用程式具有代表各個執行緒間要共享的資源的類。正確設計的完全執行緒安全類不需要呼叫任何同步函式。該類的任何事情都在內部處理,使您可以將精力集中於如何更好地使用類,而不是它如何會損壞。建立完全執行緒安全類的有效技術是將同步類合併到資源類中。將同步類合併到共享類是一個簡單的過程。
    以維護連結的帳戶列表的應用程式為例。此應用程式允許在獨立的視窗中最多檢查三個帳戶,但是在任何特定的時間,只能更新一個帳戶。更新帳戶後,通過網路將更新的資料傳送到資料存檔。
    此示例應用程式使用所有這三種類型的同步類。因為它一次最多允許檢查三個帳戶,所以它使用 CSemaphore 限制對三個檢視物件的訪問。當試圖檢視第四個帳戶時,應用程式或者等到前三個視窗中有一個關閉,或者該嘗試失敗。更新帳戶時,應用程式使用 CCriticalSection 確保一次只更新一個帳戶。更新成功後,發出訊號 CEvent 以釋放等待該事件訊號傳送的執行緒。此執行緒將新資料傳送到資料存檔。

4. 設計執行緒安全類

    若要使類完全執行緒安全,首先將適當的同步類作為資料成員新增到共享類中。在前面的帳戶管理示例中,將 CSemaphore 資料成員新增到檢視類,將CCriticalSection 資料成員新增到連結的列表類,將 CEvent 資料成員新增到資料儲存類。
    下一步,將同步呼叫新增到修改類中的資料或訪問受控資源的所有成員函式中。應該在每個函式中建立 CSingleLock 或 CMultiLock 物件,並呼叫該物件的Lock 函式。當鎖定物件超出範圍並被銷燬時,該物件的解構函式呼叫 Unlock 以釋放資源。當然,如果願意,可直接呼叫Unlock
    用這種方式設計執行緒安全類使得在多執行緒應用程式中使用該類與使用非執行緒安全類一樣容易,但卻具有更高的安全級別。將同步物件和同步訪問權物件封裝到資源的類將提供完全執行緒安全程式設計的所有優點,而不會有維護同步程式碼的缺點。
    下面的程式碼示例通過使用在共享資源類和 CSingleLock

物件中宣告的資料成員 m_CritSectionCCriticalSection 型別),對此方法進行了說明。通過使用m_CritSection 物件的地址建立 CSingleLock 物件,來試圖同步共享資源(從 CWinThread 派生)。試圖鎖定資源,一旦鎖定,即完成了共享物件上的工作。完成工作後,即呼叫 Unlock 取消鎖定資源。

CSingleLock singleLock(&m_CritSection);
singleLock.Lock();
// resource locked
//.usage of shared resource...

singleLock.Unlock();

此方法的缺點是此類將要比沒有新增同步物件的相同類慢一些。而且,如果有一個以上的執行緒可能刪除物件,合併方法不一定始終有效。在這種情況下,最好維持單獨同步物件。

5. CSemaphore

    一個CSemaphore類物件代表一個“訊號”——一個同步物件,它允許有限數目的執行緒在一個或多個程序中訪問同一個資源。一個CSemaphore物件保持了對當前訪問某一指定資源的執行緒的計數。
    CSemaphore物件用於控制有限數量的使用者使用共享資源。它的當前計數是指還可以允許的其它使用者的數目。當這個計數達到零的時候,所有對這個由CSemaphore物件控制的資源的訪問嘗試都將被插入到一個系統佇列中等待,直到它們的時間用完或計數值不再為零。這個被控制的資源可以同時接受訪問的最大使用者數目是在CSemaphore物件的構造期間被指定的。 
    要使用一個CSemaphore物件,則在需要的時候構造這個物件,指定你想要等待的訊號的名字,應用程式應該在最初就擁有它。然後你就可以在建構函式返回時訪問這個訊號。當你要訪問這個被控制的資源時,呼叫CSyncObject::Unlock。 
    使用CSemaphore物件的另一種方法是,將一個CSemaphore型別的變數新增到你想要控制的類中作為一個數據成員。在被控制物件的構造期間,呼叫CSemaphore資料成員的建構函式來指定訪問計數的初始值,訪問計數的最大值,訊號的名字(如果它要在整個程序中使用),以及需要的安全標誌。
    要訪問由CSemaphore物件用這種方式控制的資源,首先要在你的資源的訪問成員函式中建立一個CSingleLock型別或CMultiLock型別的變數。然後呼叫加鎖物件的Lock成員函式(例如,CSingleLock::Lock)。這時,你的執行緒將達到對資源的訪問,等待資源被釋放並訪問它,或者是在等待資源被釋放的過程中超過了時間,對資源的訪問失敗。不管是哪一種情況,你的資源都是以一種執行緒安全(thread-safe)方式被訪問的。要釋放資源,可以使用加鎖物件的Unlock成員函式(例如,CSingleLock::Unlock),或者是讓加鎖物件超越範圍,析構時釋放資源。
    另外,你可以單獨建立一個CSemaphore物件,並且在嘗試訪問被控制的資源之前顯式地訪問CSemaphore物件。這種方法雖然對閱讀你的原始碼的人來說更加清楚,但是卻更易於出錯。
    CSemaphore的構造:

CSemaphore( 
        LONG lInitialCount = 1,   
        LONG lMaxCount = 1,   
        LPCTSTR pstrName = NULL,   
        LPSECURITY_ATTRIBUTES lpsaAttributes = NULL   
        ); 

    1)lInitialCount:初始訪問計數值,必須不小於0,不大於最大計數值
    2)lMaxCount :允許訪問計數的最大值,必須大於0
    3)pstrName:“訊號量”的名字,如果該物件用於程序間的處理,必須提供。如果為NULL,該物件是未命名的。如果指定名字匹配一個已存在的訊號,建構函式將引用該名稱的訊號物件構造一個新的訊號物件,如果指定的名字匹配一個已存在的非semaphore的同步物件,構造失敗。
    4)lpsaAttributes:物件的安全屬性。通常為NULL。具體參見MSDN關於SECURITY_ATTRIBUTES

6. CMutex

    CMutex類的物件代表“啞程(mutex)”——它為一個同步物件,只允許某一個執行緒獨自訪問同一資源。在僅僅一個執行緒被允許用於修改資料或其它被控制的資源時,啞程將變得非常有用。例如,給連結的列表增添一個結點就是隻允許一個執行緒的過程。通過使用CMutex物件來控制連結列表,此時只有一個執行緒能夠獲得列表的訪問權。
    若要使用CMutex 物件,首先要構造一個所需的CMutex 物件。然後指定希望等待的啞程的名稱,那麼應用最初就將擁有它。可以在建構函式返回時,訪問啞程。當你已經訪問了被控制的資源後,再呼叫CSyncObject::Unlock函式。 
    另外一種使用CMutex 物件的方法就是一個CMutex型別的變數,將其作為你希望控制類的資料成員。在被控制物件的構造過程中,若啞程最初擁有了啞程的名稱或期待的安全屬性,那麼就呼叫CMutex資料成員指定的建構函式。以這種方式訪問由CMutex 物件控制的資源,首先要在資源訪問的成員函式中建立CSingleLock型別或CMultiLock型別的變數。然後呼叫封鎖物件的Lock成員函式(例如, CSingleLock::Lock)。這樣,你的執行緒要麼就獲得資源的訪問權,以等待將要釋放的資源,並獲取訪問權,要麼就等待將要釋放的資源,當超時後,返回失敗。在任何一種情況下,都可以線上程安全的模式下訪問資源。若要釋放這些資源,使用封鎖物件的Unlock成員函式(例如, CSingleLock::Unlock),或允許封鎖物件越界。
    CMutex的建構函式:

CMutex(
   BOOL bInitiallyOwn = FALSE,
   LPCTSTR lpszName = NULL,
   LPSECURITY_ATTRIBUTES lpsaAttribute = NULL 
);

    1)bInitiallyOwn:如果執行緒建立CMutex物件最初就通過mutex控制訪問資源,就指定該值
    2)lpszName :CMutex的名字。如果該物件用於程序間處理,必須指定該名字,如果為NULL,則該物件是未命名的。如果已存在另一個同名的mutex,構造器將引用那個已存在的物件建立新的CMutex物件,如果已存在另一個同名但非mutex物件的同步類,構造將失敗。
    3)lpsaAttribute:安全屬性,通常為NULL

7. CCriticalSection

    類CCriticalSection的物件表示一個“臨界區”,它是一個同步物件,同一時刻只允許一個執行緒訪問資源或程式碼區。臨界區用於控制一次只有一個執行緒修改資料或其它的控制資源。例如,在連結串列中增加一個結點就只允許一次一個執行緒進行。通過使用CCriticalSection物件來控制連結串列,就可以達到這個目的。
    CCriticalSection物件的功能由當前的Win32物件--CRITICAL_SECTION提供。
    在執行效能比較重要而且資源不會跨程序使用時,建議採用臨界區代替Mutex。使用CCriticalSection物件之前,需要構造它。在建構函式返回後,就可以使用臨界區了。在使用完之後要呼叫UnLock函式。
    存取由CCriticalSection控制的資源時,要在資源的存取函式中定義一個CSingleLock型的變數。然後呼叫加鎖物件的Lock成員函式(如CSingleLock::Lock)。此時,呼叫的執行緒要麼獲得對資源的存取權,要麼等待他人釋放資源等待加鎖,或者等待他人釋放資源,但又因為超時而加鎖失敗。這樣就保證了一次只有一個執行緒在存取臨界資源。釋放資源只需呼叫成員函式UnLock(例如CSingleLock:Unlock),或讓鎖物件在作用範圍之外。
 此外,可以單獨地建立一個CCriticalSection物件,並在存取臨界資源之前顯式地存取它。這種方式有助於保持程式碼的清晰,但是更容易出錯,因為程式設計師要記住在存取臨界資源前加鎖,存取之後開鎖。

8. CEvent

    CEvent類物件,表示一個“事件”——一個允許一個事件發生時執行緒通知另一個執行緒的同步物件。在一個執行緒需要了解何時執行任務時,事件是十分有用的。例如,拷貝資料到資料文件時,執行緒應被通知何時資料是可用的。當新資料可用時,通過運用CEvent物件來通知拷貝執行緒,執行緒才可能儘快地執行。 
    CEvent物件有兩種型別:自動和手工。一個手工CEvent物件存在於由ResetEvent 或SetEvent設定的狀態中,直到另一個函式被呼叫。一個自動CEvent物件在至少一個執行緒被釋放後自動返回一個無標記(無用的)狀態。
    要使用一個CEvent物件,應在需要時構造一個CEvent物件。指定要等待的事件的名字,最初擁有它的程序,就可以在建構函式返回時訪問事件。呼叫SetEvent標記(使可用)事件物件,然後當訪問完控制資源時,呼叫Unlock函式。
    另一個使用CEvent物件的方法是新增一個CEvent型別的變數,使之成為希望控制的類的一個數據成員。在控制物件被構造期間,可呼叫CEvent資料成員的建構函式,它指明時間是否是最初就被標記、需要的事件物件型別、事件名稱(如果在程序中要使用)和所希望的安全屬性。 
    按以下方式訪問一個被CEvent物件控制的資源:首先建立在資源訪問成員函式中構造一個CSingleLock或CMultiLock型別的變數,然後呼叫封鎖物件的Lock成員函式(如CMultiLock::Lock)。此時,執行緒要麼可以訪問資源,等待資源釋放後訪問;要麼等待資源釋放而超時,訪問資源失敗。在各種情況下,資源都被以執行緒安全方式訪問。要釋放資源,可呼叫SetEvent來標識一個事件物件,然後使用封鎖物件的Unlock成員函式(如CMultiLock::UnLock),或允許封鎖物件超過範圍。

9. CSingleLock

    它代表用於多執行緒程式中資源訪問的控制機制。
    為了使用同步物件,必須建立同步訪問類物件CSingleLock或CMultiLock 等待或釋放該同步物件。當只需要同一時間等待一個同步物件,使用CSingleLock;當某一特定時刻需要等待多個同步物件,使用CMultiLock。
    要使用CSingleLock,在資源訪問類得成員函式中構造該物件,然後呼叫該物件的成員函式IsLocked檢測資源是否可用,如果是,繼續執行資源訪問類的該成員函式的剩餘部分,如果資源不可用,等待特定是時間直到資源訪問控制被釋放或者訪問資源失敗。當資源訪問完成時,呼叫Unlock釋放資源控制權,或使構造的CSingleLock物件析構。

10. CMultiLock

    它代表用於多執行緒程式中資源訪問的控制機制。
    為了使用同步物件,必須建立同步訪問類物件CSingleLock或CMultiLock 等待或釋放該同步物件。當只需要同一時間等待一個同步物件,使用CSingleLock;當某一特定時刻需要等待多個同步物件,使用CMultiLock。
    要使用CMultiLock,首先建立你想要等待的同步物件陣列,接著,在再資源訪問類的成員函式中構造CMultiLock物件,然後,呼叫CMultiLock的Lock 檢測資源是否有效(有訊號)。如果有資源有效,繼續資源訪問,如果沒有資源有效,等待資源訪問控制權釋放或訪問資源失敗。訪問資源完成後,呼叫Unlock 釋放資源控制權,或使CMultiLock超出作用域而析構。