C++ 線程同步的四種方式

lrx137 8年前發布 | 33K 次閱讀 線程 C/C++開發 C/C++

線程之間通信的兩個基本問題是互斥和同步。

(1)線程同步是指線程之間所具有的一種制約關系,一個線程的執行依賴另一個線程的消息,當它沒有得到另一個線程的消息時應等待,直到消息到達時才被喚醒。

(2)線程互斥是指對于共享的操作系統資源(指的是廣義的”資源”,而不是Windows的.res文件,譬如全局變量就是一種共享資源),在各線程訪問時的排它性。當有若干個線程都要使用某一共享資源時,任何時刻最多只允許一個線程去使用,其它要使用該資源的線程必須等待,直到占用資源者釋放該資源。

線程互斥是一種特殊的線程同步。實際上,互斥和同步對應著線程間通信發生的兩種情況:

(1)當有多個線程訪問共享資源而不使資源被破壞時;

(2)當一個線程需要將某個任務已經完成的情況通知另外一個或多個線程時。

從大的方面講,線程的同步可分用戶模式的線程同步和內核對象的線程同步兩大類。用戶模式中線程的同步方法主要有原子訪問和臨界區等方法。其特點是同步速度特別快,適合于對線程運行速度有嚴格要求的場合。

內核對象的線程同步則主要由 事件等待定時器信號量 以及 信號燈 等內核對象構成。由于這種同步機制使用了內核對象,使用時必須將線程從用戶模式切換到內核模式,而這種轉換一般要耗費近千個CPU周期,因此同步速度較慢,但在適用性上卻要遠優于用戶模式的線程同步方式。

在WIN32中,同步機制主要有以下幾種:

(1)事件(Event);

(2)信號量(semaphore);

(3)互斥量(mutex);

(4)臨界區(Critical section)。

臨界區

臨界區(Critical Section)是一段獨占對某些共享資源訪問的代碼,在任意時刻只允許一個線程對共享資源進行訪問。如果有多個線程試圖同時訪問臨界區,那么在有一個線程進入后其他所有試圖訪問此臨界區的線程將被掛起,并一直持續到進入臨界區的線程離開。臨界區在被釋放后,其他線程可以繼續搶占,并以此達到用原子方式操作共享資源的目的。

臨界區在使用時以CRITICAL_SECTION結構對象保護共享資源,并分別用EnterCriticalSection()和LeaveCriticalSection()函數去標識和釋放一個臨界區。所用到的CRITICAL_SECTION結構對象必須經過InitializeCriticalSection()的初始化后才能使用,而且必須確保所有線程中的任何試圖訪問此共享資源的代碼都處在此臨界區的保護之下。否則臨界區將不會起到應有的作用,共享資源依然有被破壞的可能。

全局變量

因為進程中的所有線程均可以訪問所有的全局變量,因而全局變量成為Win32多線程通信的最簡單方式。例如:

int var; //全局變量
UINTThreadFunction(LPVOIDpParam)
{
    var = 0;
    while (var < MaxValue)
    {
        //線程處理
        ::InterlockedIncrement(long*) &var);
    }
    return 0;
}

請看下列程序:

int globalFlag = false; 
DWORDWINAPIThreadFunc(LPVOID n)
{
    Sleep(2000);
    globalFlag = true;
 
    return 0;
}
 
int main()
{
    HANDLEhThrd;
    DWORDthreadId;
 
    hThrd = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &threadId);
    if (hThrd)
    {
        printf("Thread launched\n");
        CloseHandle(hThrd);
    }
 
    while (!globalFlag)
    ;
    printf("exit\n");
}

上述程序中使用全局變量和while循環查詢進行線程間同步,實際上,這是一種應該避免的方法,因為:

(1)當主線程必須使自己與ThreadFunc函數的完成運行實現同步時,它并沒有使自己進入睡眠狀態。由于主線程沒有進入睡眠狀態,因此操作系統繼續為它調度C P U時間,這就要占用其他線程的寶貴時間周期;

(2)當主線程的優先級高于執行ThreadFunc函數的線程時,就會發生globalFlag永遠不能被賦值為true的情況。因為在這種情況下,系統決不會將任何時間片分配給ThreadFunc線程。

事件

事件(Event)是WIN32提供的最靈活的線程間同步方式,事件可以處于激發狀態(signaled or true)或未激發狀態(unsignal or false)。根據狀態變遷方式的不同,事件可分為兩類:

(1)手動設置:這種對象只可能用程序手動設置,在需要該事件或者事件發生時,采用SetEvent及ResetEvent來進行設置。

(2)自動恢復:一旦事件發生并被處理后,自動恢復到沒有事件狀態,不需要再次設置。

使用”事件”機制應注意以下事項:

(1)如果跨進程訪問事件,必須對事件命名,在對事件命名的時候,要注意不要與系統命名空間中的其它全局命名對象沖突;

(2)事件是否要自動恢復;

(3)事件的初始狀態設置。

由于event對象屬于內核對象,故進程B可以調用OpenEvent函數通過對象的名字獲得進程A中event對象的句柄,然后將這個句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函數中。此法可以實現一個進程的線程控制另一進程中線程的運行,例如:

HANDLEhEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent"); 
ResetEvent(hEvent);

信號量

信號量是維護0到指定最大值之間的同步對象。信號量狀態在其計數大于0時是有信號的,而其計數是0時是無信號的。信號量對象在控制上可以支持有限數量共享資源的訪問。

信號量的特點和用途可用下列幾句話定義:

(1)如果當前資源的數量大于0,則信號量有效;

(2)如果當前資源數量是0,則信號量無效;

(3)系統決不允許當前資源的數量為負值;

(4)當前資源數量決不能大于最大資源數量。

創建信號量

 HANDLECreateSemaphore (
      PSECURITY_ATTRIBUTEpsa,
      LONG lInitialCount, //開始時可供使用的資源數
      LONG lMaximumCount, //最大資源數
  PCTSTRpszName);

釋放信號量

通過調用ReleaseSemaphore函數,線程就能夠對信標的當前資源數量進行遞增,該函數原型為:

BOOL WINAPIReleaseSemaphore(
      HANDLEhSemaphore,
      LONG lReleaseCount, //信號量的當前資源數增加lReleaseCount
      LPLONGlpPreviousCount
  );

打開信號量

和其他核心對象一樣,信號量也可以通過名字跨進程訪問,打開信號量的API為:

 HANDLEOpenSemaphore (
      DWORDfdwAccess,
      BOOL bInherithandle,
      PCTSTRpszName
  );

互鎖訪問

當必須以原子操作方式來修改單個值時,互鎖訪問函數是相當有用的。所謂原子訪問,是指線程在訪問資源時能夠確保所有其他線程都不在同一時間內訪問相同的資源。

請看下列代碼:

int globalVar = 0;
 
DWORDWINAPIThreadFunc1(LPVOID n)
{
    globalVar++;
    return 0;
}
DWORDWINAPIThreadFunc2(LPVOID n)
{
    globalVar++;
    return 0;
}

運行ThreadFunc1和ThreadFunc2線程,結果是不可預料的,因為globalVar++并不對應著一條機器指令,我們看看globalVar++的反匯編代碼:

00401038 moveax,[globalVar (0042d3f0)]
0040103D addeax,1
00401040 mov [globalVar (0042d3f0)],eax

在”mov eax,[globalVar (0042d3f0)]” 指令與”add eax,1″ 指令以及”add eax,1″ 指令與”mov [globalVar (0042d3f0)],eax”指令之間都可能發生線程切換,使得程序的執行后globalVar的結果不能確定。我們可以使用InterlockedExchangeAdd函數解決這個問題:

int globalVar = 0;
 
DWORDWINAPIThreadFunc1(LPVOID n)
{
    InterlockedExchangeAdd(&globalVar,1);
    return 0;
}
DWORDWINAPIThreadFunc2(LPVOID n)
{
    InterlockedExchangeAdd(&globalVar,1);
    return 0;
}

InterlockedExchangeAdd保證對變量globalVar的訪問具有”原子性”。互鎖訪問的控制速度非常快,調用一個互鎖函數的CPU周期通常小于50,不需要進行用戶方式與內核方式的切換(該切換通常需要運行1000個CPU周期)。

互鎖訪問函數的缺點在于其只能對單一變量進行原子訪問,如果要訪問的資源比較復雜,仍要使用臨界區或互斥。

可等待定時器

可等待定時器是在某個時間或按規定的間隔時間發出自己的信號通知的內核對象。它們通常用來在某個時間執行某個操作。

創建可等待定時器

 HANDLECreateWaitableTimer(
              PSECURITY_ATTRISUTESpsa,
              BOOL fManualReset,//人工重置或自動重置定時器
            PCTSTRpszName);

設置可等待定時器

可等待定時器對象在非激活狀態下被創建,程序員應調用 SetWaitableTimer函數來界定定時器在何時被激活:

BOOL SetWaitableTimer(
              HANDLEhTimer, //要設置的定時器
              const LARGE_INTEGER *pDueTime, //指明定時器第一次激活的時間
              LONG lPeriod, //指明此后定時器應該間隔多長時間激活一次
              PTIMERAPCROUTINEpfnCompletionRoutine,
              PVOIDPvArgToCompletionRoutine,
            BOOL fResume);

取消可等待定時器

BOOL CancelWaitableTimer(
                HANDLEhTimer //要取消的定時器
        );

打開可等待定時器

作為一種內核對象,WaitableTimer也可以被其他進程以名字打開:

HANDLEOpenWaitableTimer (
                DWORDfdwAccess,
                BOOL bInherithandle,
                PCTSTRpszName
        );

實例

下面給出的一個程序可能發生死鎖現象:

#include <windows.h>
#include <stdio.h>
CRITICAL_SECTIONcs1, cs2;
long WINAPIThreadFn(long);
main()
{
    long iThreadID;
    InitializeCriticalSection(&cs1);
    InitializeCriticalSection(&cs2);
    CloseHandle(CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFn, NULL, 0,&iThreadID));
    while (TRUE)
    {
        EnterCriticalSection(&cs1);
        printf("\n線程1占用臨界區1");
        EnterCriticalSection(&cs2);
        printf("\n線程1占用臨界區2");
 
        printf("\n線程1占用兩個臨界區");
 
        LeaveCriticalSection(&cs2);
        LeaveCriticalSection(&cs1);
 
        printf("\n線程1釋放兩個臨界區");
        Sleep(20);
    };
    return (0);
}
 
long WINAPIThreadFn(long lParam)
{
    while (TRUE)
    {
        EnterCriticalSection(&cs2);
        printf("\n線程2占用臨界區2");
        EnterCriticalSection(&cs1);
        printf("\n線程2占用臨界區1");
 
        printf("\n線程2占用兩個臨界區");
 
        LeaveCriticalSection(&cs1);
        LeaveCriticalSection(&cs2);
 
        printf("\n線程2釋放兩個臨界區");
        Sleep(20);
    };
}

運行這個程序,在中途一旦發生這樣的輸出:

線程1占用臨界區1 線程2占用臨界區2

線程2占用臨界區2 線程1占用臨界區1

線程1占用臨界區2 線程2占用臨界區1

線程2占用臨界區1 線程1占用臨界區2

程序就”死”掉了,再也運行不下去。因為這樣的輸出,意味著兩個線程相互等待對方釋放臨界區,也即出現了死鎖。

如果我們將線程2的控制函數改為:

long WINAPIThreadFn(long lParam)
{
    while (TRUE)
    {
        EnterCriticalSection(&cs1);
        printf("\n線程2占用臨界區1");
        EnterCriticalSection(&cs2);
        printf("\n線程2占用臨界區2");
 
        printf("\n線程2占用兩個臨界區");
 
        LeaveCriticalSection(&cs1);
        LeaveCriticalSection(&cs2);
 
        printf("\n線程2釋放兩個臨界區");
        Sleep(20);
    };
}

再次運行程序,死鎖被消除,程序不再擋掉。這是因為我們改變了線程2中獲得臨界區1、2的順序,消除了線程1、2相互等待資源的可能性。

由此我們得出結論,在使用線程間的同步機制時,要特別留心死鎖的發生。

 

來自:http://blog.jobbole.com/109200/

 

 本文由用戶 lrx137 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!