線程同步

一、用戶方式中的線程同步

1.互鎖函數

       可以使用InterlockedExchangeAdd函數對一個長變量以原子操作的方式遞增一個值:

LONG InterlockedExchangedAdd(
    PLONG plAddend,
    LONG lIncrement
);

      可以使用InterlockedExchange和InterlockedExchangePointer函數以原子操作的方式用第二個參數的值替換第一個參數傳遞個當前值,兩個函數都返回第一個函數的原始值。兩個函數運用在32位的應用程序,都能用32位的替換32位。用在64位的應用程序,InterlockedExchange函數用於32位,InterlockedExchangePointer函數用於取代64位值,函數如下:

LONG InterlockedExchange(
    PLONG plTarget,
    LONG lValue
);
PVOID InterlockedExchangePointer(
    PVOID *ppvTarget,
    PVOID pvValue
);

      可以使用函數InterlockedCompareExchange和InterlockedCompareExchangePointer以原子訪問的方式將第一個參數傳遞的當前值與第三個參數進行比較,如果值相同,用第二個參數的值替換當前值,如果不相同,保持當前值不變。兩個函數都返回原始值。兩個函數都能運行在32位的應用程序中。在64位的應用程序中,InterlockedCompareExchange用於比較32位的值,InterlockedCompareExchangePointer用於64位,函數如下:

PVOID InterlockedCompareExchange(
   PLONG plDestination,
   LONG lExchange,
   LONG lCommand
);
PVOID InterlockedCompareExchangePointer(
   PVOID *ppvDestination,
   PVOID pvExchange,
   PVOID pvCommand
);

      兩個比較老的函數,可與運用上面的函數來實現。分別以原子操作的方式遞增或遞減1修改長整數的函數:

LONG InterlockedIncrement(PLONG plAddend);
LONG InterlockedDecrement(PLONG plAddend);

2.高速緩存行

 當有多個CPU時,高速緩存行可能造成同步問題。例如下面這個數據結構就是非常差:

struct CUSTINFO{
  DWORD dwCustorID;// 大部分讀
  int c;  //讀寫
  char Name[100];//大部分讀
  FILETIEM Data;//大部分寫
};

因此爲了同步問題,可以用於下面的方式改進:

// Determine the cache line size for the host CPU.
//爲各種CPU定義告訴緩存行大小
#ifdef _X86_
#define CACHE_ALIGN  32
#endif
#ifdef _ALPHA_
#define CACHE_ALIGN  64
#endif
#ifdef _IA64_
#define CACHE_ALIGN  ??
#endif

#define CACHE_PAD(Name, BytesSoFar) \
   BYTE Name[CACHE_ALIGN - ((BytesSoFar) % CACHE_ALIGN)]

struct CUSTINFO
{
   DWORD    dwCustorID;     // Mostly read-only
   char     Name[100];      // Mostly read-only

   CACHE_PAD(bPad1, sizeof(DWORD) + 100);

   int      c;      // Read-write
   FILETIME Date;  // Read-write

   CACHE_PAD(bPad2, sizeof(int) + sizeof(FILETIME));
};

3.關鍵代碼段 

        在使用關鍵代碼段前要先定義CRITICAL_SECTION結構體變量。然後要運用InitializeCriticalSection函數對CRITICAL_SECTION結構進行初始化:

     VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);

        在編寫使用共享資源的代碼時,要在代碼前加入下面的函數:  

     VOID EnterCriticalSection(PCRITICAL_SECTION pcs);

         EnterCriticalSection函數會查看CRITICAL_SECTION結構的成員變量。這些變量指明哪個變量正在使用該資源,其會負責以下測試,這些測試都是以原子操作方式執行:

  • 如果沒有線程訪問該資源,那麼該函數就會更新成員變量,以指明該資源以被賦予訪問權,然後該函數返回,線程繼續運行。
  • 如果成員變量指明調用線程已經獲取了該資源的訪問權,那麼該函數會更新這些變量,以表明調用線程被賦予訪問權多少次,然後該函數會返回,線程繼續運行。一個線程多少次賦予訪問權,就需要調用多次Leave函數才能釋放該資源。
  • 如果成員變量指明另一個線程已經被賦予了對該資源的訪問權,那麼調用線程就會進入等待狀態。系統會記住該線程想獲取該資源的訪問權,並更新CRITICAL_SECTION結構成員變量,當線程釋放該資源時,等待線程變爲可調用狀態。

        可以使用TryEnterCritcalSection函數來代替EnterCriticalSection函數,該函數會進行相同的測試,如果能夠訪問共享資源,其會返回TRUE,線程獲取訪問權,繼續執行,釋放該資源需要調用LeaveCriticalSection函數;如果不能獲取訪問共享資源的訪問權,那麼會返回FLASE,但是這與EnterCriticalSection函數不同,該函數不會讓調用線程進入等待狀態。其形式如下:

       BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);

         在共享代碼結尾,需要使用LeaveCriticalSection函數。該函數會查看CRITICAL_SECTION結構的成員變量,將計數減1。如果計數爲0,那麼就會查看是否有其他線程調用EnterCriticalSection函數處於等待狀態,如果有,那麼就會更新成員變量,該線程處於可調度狀態;如果沒有,那麼該函數更新成員變量,以表明沒有線程正在訪問該共享資源;如果計數不爲0,該函數只會返回。函數形式如下:

       VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);

        線程當無法獲取共享資源時,會進入等待狀態,會由用戶方式進入內核方式,這樣會付出時間和資源代價。因此,windows引入了將循環鎖加入了代碼段,我們可以運用InitializeCriticalSectionAndSpinCount函數來實現,其形式如下:

BOOL InitializeCriticalSectionAndSpinCount(
  PCRITICAL_SECTION pcs,
  DWORD dwSpinCount  //用於設置循環的次數
);

        可以使用SetCriticalSectionSpinCount函數來改變關鍵代碼段循環次數:

BOOL SetCriticalSectionSpinCount(
  PCRITICAL_SECTION pcs,
  DWORD dwSpinCount  //用於設置循環的次數
);

       使用關鍵代碼段的技巧:

  • 每個共享資源使用一個CRITICAL_SECTION變量;
  • 同時訪問多個資源時,可以設置多個CRITICAL_SECTION變量;
  • 不要長時間允許關鍵代碼段

二、內核對象實現線程同步

       儘管用戶方式相對於內核對象方式的線程同步速度快,但其有許多侷限和不足:例如互鎖函數只能用於單值;關鍵代碼段只能在單個進程中線程同步,並且容易死鎖。
1、等待函數

       等待函數可以讓函數進入等待狀態,知道等待的內核對象變爲已通知狀態。等待單個內核對象函數WaitForSingleObject:

DWORD WaitForSingleObject(
  HANDLE hObject,
  DWORD dwMilliseconds
);
  • hObject:其爲能夠變爲未通知/通知狀態的內核對象。
  • dwMilliseconds:其爲等待函數等待的時間。如果其爲INFINITE表示等待函數一直等待,除非內核對象變爲已通知;也可以設置爲其他值(以毫秒爲單位),如果到等待時間結束前,內核對象還沒變爲已通知,那麼就不會再等待,執行調用線程。
  • 返回值:WAIT_OBJECT_0:表示內核對象已變爲通知狀態;WAIT_OTIMEOUT:表示超時;WAIT_FAILED:表示錯誤

      可以使用WaitForMultipleObjects函數等待多個內核對象,WaitForMultipleObjects以原子操作的方式檢查內核對象的狀態:

DWORD WaitForSingleObject(
  DOWRD dwCount,
  CONST HANDLE *phObjects,
  BOOL fWaitAll,
  DWORD dwMilliseconds
);
  • dwCount:表示等待的內核對象的數目
  • phObjects:等待內核對象句柄數組
  • fWaitAll:設爲TRUE,表示所有內核對象都變爲已通知狀態才能返回;爲FALSE,表示只要有一個內核對象變爲已通知狀態,就可以返回
  • dwMillisecond:等待時間。同WaitForSingleObject
  • 返回值:WAIT_OTIMEOUT:表示超時;WAIT_FAILED:表示錯誤;如果fWaitAll爲TRUE,並且所有的對象變爲已通知狀態,返回WAIT_OBJECT_0;如果fWaitAll爲FALSE,如果一個內核對象變爲已通知,會返回WAIT_OBJECT_0到WAIT_OBJECT_0+dwCount-1之間的一個值,表示已變爲通知狀態的內核對象在內核對象句柄數組中的下標。

2.事件內核對象

        事件內核對象包括一個引用計數、一個用於標識是自動重置還是人工重置的布爾值、一個用於標識是已通知狀態還是未通知狀態的布爾值。

        事件內核對象有兩種類型:人工重置類型和自動重置類型。如果人工重置事件內核對象得到通知時,等待該事件的所有線程都變爲可調度狀態;如果是自動重置,等待該事件的線程只有一個線程能夠變爲可調度狀態。

        通過函數CreateEvent函數創建事件內核對象:

HANDLE CreateEvent(
  PSECURITY_ATTRIBUTES psa,
  BOOL fManuaReset,
  BOOL bInitialState,
  PCTSTR  pszName
);
  • psa:設置內核對象的安全性;
  • fManuaReset:設置事件內核對象的類型:人工重置和自動重置。TRUE爲人工重置,FALSE爲自動重置;
  • fInitialState:爲初始化事件對象的狀態:TRUE爲已通知狀態,FALSE爲未通知狀態。
  • pszName:給事件對象命名。

       可以使用OpenEvent函數來打開已經存在的時間內核對象:

HANDLE OpenEvent(
   DWORD fdwAcess,
   BOOL fInherit,
   PCTSTR pszName
);

       可以通過函數SetEvent將事件設爲已通知狀態,通過函數ResetEvent函數將事件設爲未通知狀態:

BOOL SetEvent(HANDLE hEvent);
BOOL ResetEvent(HANDLE hEvent);

       自動重置事件相對於人工重置事件有等待成功副作用:當成功等待該對象時,該對象將由已通知狀態變爲未通知狀態。
       內核對象都需要調用CloseHandle函數來釋放該進程使用的內核對象。

        PulseEvent函數可以讓事件變爲已通知狀態,然後立即又變爲未通知狀態。其可以等待線程變爲可調度。

BOOL PulseEvent(HANDLE hEvent);

3.等待定時器內核對象

        等待定時器內核對象就是在某個時間或按時間間隔發出通知的內核對象。等待定時器和事件一樣,也分爲人工重置定時器和自動重置定時器:如果人工重置定時器發出通知時,所有等待線程都處於可調度狀態;如果自動重置定時器發出通知,那麼等待線程中的一個線程可以處於可調度狀態。
       可以使用函數CreateWaitableTimer創建等待定時器和使用函數OpenWaitableTimer函數打開等待定時器,函數如下:

HANDLE CreateWaitableTimer(
   PSECURITY_ATTRIBUTES psa,
   BOOL fManualReset,
   PCTSTR pszName
);
HANDLE OpenWaitableTimer(
   DWOD dwDesiredAccess,
   BOOL fInherit,
   PCTSTR pszName
);

       等待定時器總是在未通知狀態下創建的,因此需要的調用SetWaitableTimer函數來設置定時器在何時成爲已通知狀態,函數如下:

BOOL SetWaitableTimer(
   HANDLE hTimer,
   const LARGE_INTEGER *pDueTime,
   LONG lPeriod,
   PTIMERAPCROUTINE pfnCompletionRoutine,
   PVOID pvArgToCompletionRoutine,
   BOOL fResume
);
  • hTimer:等待定時器句柄
  • pDueTime:指明定時器第一次何時報時
  • lPeriod:指明定時器隔多久後再次報時
  • pfnCompletionRoutine:回調函數地址
  • pvArgToCompletionRoutine:參數

回調函數的形式:

VOID APIENTRY name(PVOID pfnCompletionRoutine,DWORD dwTimerLowValue,DWORD dwTimerHeighValue)
{
}

        可以調用函數CancelWaitableTimer函數撤銷定時器報時:

BOOL CancelWaitableTimer(HANDLE hTimer);

4.信標內核對象

        信標內核對象用於對資源計數。其包括一個引用計數、一個32位值用於表示最大資源數、一個32位值表示當前資源數。當前資源數大於0,小於最大資源數,且不爲0時,發出信標信號。當前資源數爲0時,不發出信標信號;當前資源數不能爲負值,且當前資源數不能大於最大資源數。

       使用函數CreateSemahore創建信標內核對象,使用OpenSemahore函數打開一個已經存在的信標內核對象,函數如下:

HANDLE CreateSemaphore(
    PSECURITY_ATTRIBUTE psa,
    LONG lInitialCount,
    LONG iMaximumCount,
    PCTSTR pszName
);
HANDLE OpenSemaphore(
    DW0RD fdwAccess,
    BOOL bInheritHandle,
    PCTSTR pszName
);

         等待函數對於信標內核對象的副作用:信標的當前資源數減1.

         可以使用函數ReleaseSemphore函數對當前資源數遞增:  

LONG ReleaseSemaphore(
   LONG lsem,
   LONG lReleaseCount,//添加的資源數
   PLONG plPreviousCount//原始當前資源數
);

注意:如果不對當前資源做修改的話,無法獲取資源數。如果調用ReleaseSemphore法術的第二個參數設爲0,第三個參數會返回0;如果設爲一個大數,大於最大資源數,也會返回0.
5.互斥內核對象

        互斥內核對象包括一個引用計數、一個線程ID和一個計數器。互斥內核對象和關鍵代碼的特性比較相似,但是互斥內核對象速度比較慢,當相比於關鍵代碼段能夠跨進程同步。線程D用於標識互斥內核對象被誰擁有,計數器用於記錄該線程對互斥內核對象的擁有次數。互斥內核對象和關鍵代碼段一樣能夠被一個線程多次擁有,如果一個線程想釋放該互斥內核對象,需要對應調用釋放函數的次數要和計數器的值相同。

        使用:

  • 如果線程ID爲0,表示該互斥內核對象沒有被任何線程擁有,該互斥內核對象爲已通知狀態;
  • 如果線程ID不爲0,表示該互斥內核對象被一個線程擁有,爲未通知狀態;
  • 如果一個線程試圖獲取該互斥內核對象,且線程ID不爲0,那麼系統會檢查擁有該互斥內核對象的線程ID是否與試圖擁有互斥對象的調用線程的ID是否相同,如果相同,該調用線程繼續執行,並且互斥內核對象的計數器會加1;如果不相同,調用線程會進入等待狀態。

       可以使用函數CreateMutex創建互斥內核對象,使用OpenMutex函數打開已經存在互斥內核對象,函數如下:

HANDLE CreateEvent(
    PSECURITY_ATTRIBUTE psa,
    LONG lInitialOwner,//用於設置初始狀態
    PCTSTR pszName
);
HANDLE OpenEvent(
    DW0RD fdwAccess,
    BOOL bInheritHandle,
    PCTSTR pszName
);

       可以使用函數ReleaseEvent函數來釋放對資源的訪問權:

BOOL ReleaseEvent(HANDLE hEvent);

       線程調用ReleaseEvent函數釋放互斥對象時,系統會將調用線程和內核對象的線程ID比較,如果相同,則計數器會減1,如果計數器的值變爲0,則將線程ID置爲0,如果不爲0,保持線程ID不變;如果線程ID和調用線程ID不相同,該函數會返回給調用線程FALSE。
       如果擁有互斥對象的線程被終止運行,沒有釋放互斥對象,那麼系統會認爲該互斥對象被丟棄,系統會將線程ID復置爲0,並將計數器也復置爲0。然後系統會對等待線程進行處理。

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