天天看點

Windows程式設計——線程篇(二):線程同步Windows程式設計——線程篇(二):線程同步

文章目錄

  • Windows程式設計——線程篇(二):線程同步
    • 為什麼需要同步
      • 資源競争
      • 高速緩存行
    • 使用者空間線程同步
        • 原子通路
      • 臨界區線程同步
        • 臨界區
        • 臨界區與循環鎖
        • 臨界區的錯誤處理
      • 臨界區技巧
    • 核心對象線程同步
      • 等待核心對象
        • 等待函數
        • 等待線程
        • 等待多個程序
        • 等待成功的副作用
      • 事件核心對象線程同步
      • 可等待的定時器核心對象線程同步
        • 等待定時器給APC(異步過程調用)進行排隊
        • 定時器的松散特性
      • 信号量線程同步
      • 互斥體線程同步
        • 互斥體與臨界區比較

Windows程式設計——線程篇(二):線程同步

為什麼需要同步

資源競争

假如存在2個線程A、B,和資源T,A需要對int T進行讀取操作,B需要進行寫,并假設讀寫操作不是原子操作。那麼,當A讀取T的值得時候,如果B進行寫,則A讀取到的值講是不确定的。而所謂原子操作就是某一操作在完成之前這個資源不會被其他線程所通路。

高速緩存行

所謂高速緩存行,就是指CPU在從記憶體讀取一個位元組的時候,并不是讀取一個位元組,而是取出足夠的位元組來填入高速緩存行。一般高速緩存行的大小為32或者64位元組,當所需要通路的位元組存在于高速緩存行的時候,CPU就不必去通路記憶體總線,進而提高存取效率。但這也會造成資源同步的問題,同時 這也會為什麼存在register關鍵字的原因。

使用者空間線程同步

原子通路

線程同步問題在很大程度上與原子通路有關,所謂原子通路,是指線程在通路資源時能夠

確定所有其他線程都不在同一時間内通路相同的資源。

unsigned InterlockedExchange(unsigned volatile *Target,_In_ unsigned Value);
unsigned InterlockedExchangeAdd(PLONG plAdded,LONG lIncrement);
PVOID IntergerlockedCompareExchange(PLONG plDestination,LONG lExchange,LONG lComparand);
PVOID IntergerlockedCompareExchangePointer(PVOID *ppvDestination,PVOID pvExchange,PVOID pvComparand);
           

臨界區線程同步

臨界區

臨界區是指一個小代碼段,在代碼能夠執行前,它必須獨占對某些共享資源的通路權。線上程退出關鍵代碼段之前,系統将不給想要通路相同資源的其他任何線程進行排程。
// 進入臨界區
// 1.如果沒有其他線程通路該資源,則進入進階區,以指明調用線程已被賦予通路權限并立即傳回
// 2.如果已經有其他線程被賦予通路權限,那麼将進行等待,等待不會浪費CPU資源,一旦目前通路該資源的線程調用leave函數,系統将通知所有等待中的線程對該資源進行競争
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

// 離開臨界區
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

// 在任何企圖進入臨界區的線程之前調用初始化函數,否則後果是不可預料的
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);

// 删除臨界區資源
VOID DeleteCriticalSection(PCRITICAL_SECION &pcs);

// 測試是否可以進入臨界區,可以傳回TRUE,否則傳回FALSE
BOOL TryEnterCriticalSection(PCRITICAL_SECTION &pcs);

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

    //
    //  The following three fields control entering and exiting the critical
    //  section for the resource
    //

    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;        // from the thread's ClientId->UniqueThread
    HANDLE LockSemaphore;
    ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;
           
在使用enter和leave函數之前,先建立一個CRITICAL_SECTION對象(一般是全局變量),并且在使用這個對象之前必須調用初始化函數,然後在臨界區前後分别調用enter和leave即可。

臨界區與循環鎖

當使用者阻塞在entercriticalsection的時候,該線程将從使用者态切換到核心态,這個時間大概花費1000個CPU周期,這種轉換是要付出很大代價的。同時,如果是多核CPU的情況,一個線程請求進入臨界區時另一個線程正在該臨界區執行代碼,那麼請求線程将轉換到核心态,但是另一線程很可能在轉換完成前就已經釋放了該臨界區,是以這種情況也是非常浪費CPU資源的。這時候可以使用臨界區循環鎖,如下:
BOOL InititializeCriticalSectionAndSpinCount(
	PCRITICAL_SECTION &pcs,
    DWORD dwSpinCount
);
           
dwSpinCount的作用是告知線程在等待之前它濕度獲得資源時想要循環所循環疊代的次數。當超過這個次數之後才會切換到核心态,否則将獲得臨界區并傳回。使用以下函數可以在運作時修改循環的次數。

臨界區的錯誤處理

由于臨界區初始化的時候可能失敗但是InitializeCriticalSection并不會傳回false,而是産生STATUS_NO_MEMORY異常。如果這個時候調用enter函數将會導EXCEPTION_INVALID_HANDLE,如果不希望處理異常,則應該調用InititializeCriticalSectionAndSpinCount。

臨界區技巧

  • 為每個需要共享的資源配置設定一個單獨的CRITICAL_SECTION
  • 如果 有多個臨界區需要同時通路,需要確定enter的順序!!!
  • 不要長時間在臨界區運作,因為當一個臨界區運作時,其他線程将進入等待狀态
EnterCriticalSection(&g_cs);
SOMERESTRUCT sTemp = g_s;
LeaveCriticalSection(&g_cs);

// 先取出競争的資料,不要在臨界區中做長時間的操作
SendMessage(hWndSomeWnd,WM_SOMEMSG,&sTemp,0);
           

核心對象線程同步

所謂線程與核心對象的同步,就是同步的時候需要從使用者态切換到核心态,而不是像前面的臨界區那樣可以不切換到核心态就進行同步。線程可以使自己進入等待狀态,直到一個對象變為已通知狀态。所謂已通知狀态,對于線程和程序而言就是線程或進行結束運作。當線程和進行開始運作的時候是未通知狀态,當他們結束後就變為已通知狀态。

等待核心對象

等待函數

// 讓函數自願進入等待狀态,直到一個特定的核心對象變為已通知狀态或者逾時
// WAIT_OBJECT_0:被等待的核心對象狀态變為已通知
// WAIT_TIMEOUT:等待逾時
// WAIT_FAILED:等待失敗
DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);

// nCount:等待的核心對象個數
// lpHandles:核心對象數組
//  bWaitAll:是否等待所有核心對象,FALSE隻要有任何一個變為已通知就傳回,否則需要等待所有的核心對象都變為已通知wait函數才會傳回
// dwMilliseconds:毫秒數
DWORD WaitForMultipleObjects( DWORD nCount,CONST HANDLE *lpHandles,BOOL bWaitAll, DWORD dwMilliseconds
           

等待線程

DWORD dw = WaitForSingleObject(hThread,5000);
switch(dw){
    case WAIT_OBJECT_0:
        // 線程結束
        break;
    case WAIT_TIMEOUT:
        // 等待逾時,線程未結束
        break;
    case WAIT_FAILED:
        // 等待失敗,可能是因為非法HANDLE
        break;
}
           

等待多個程序

HANDLE hProcess[3];
hProcess[0 ] = hProcess1;
hProcess[1 ] = hProcess2;
hProcess[2 ] = hProcess3;

DWORD dw = WaitForMultiObject(3,hProcess,FALSE,5000);
switch(dw)
{
    case WAIT_FALIED:
        break;
    case WAIT_OBJECT_0 + 0:
        // PROCESS 1
        break;
    case WAIT_OBJECT_0 + 1:
        // PROCESS 2
        break;
    case WAIT_OBJECT_0 + 2:
        // PROCESS 3
        break;
    case WAIT_TIMEOUT:
        // 逾時
        break;
}
           

等待成功的副作用

所謂副作用,就是當調用waitobject函數成功傳回的時候,有一些被等待的核心對象在這個時候狀态會發生改變,但是有些就不會。線程和程序核心對象狀态就不會發生改變。有些對象在wait成功傳回後他們的狀态會發生改變,比如自動事件被設定為通知狀态後wait傳回,但是wait傳回的時候事件又被設定為未通知狀态,這就是所謂的副作用(相當于wait函數在傳回前調用了resetevent)~~(這裡關于副作用可以參考windows核心程式設計,第九章,9.2節。)

事件核心對象線程同步

事件核心對象是個最基本的對象。事件能夠通知一個操作已經完成。有手動和自動重置兩種事件。當手動重置的事件得到通知時,等待該事件的所有線程都變為可排程線程。當一個手動重置的事件得到通知時,等待該事件的線程中隻有一個線程變為可排程線程。當一個線程執行初始化操作,然後通知另一個線程執行剩餘的操作時,事件使用得最多。事件開始時一般初始化為未通知狀态,當完成特定的工作後可以選擇将事件設定為已通知狀态,此時等待該事件的其他線程将得到通知變得可排程。
// 2個自動重置的EVENT
HANDLE h[] = {hAutoResetEvent1,hAutoResetEvent2};
WaitForMultiObject(2,h,TRUE,INFINITE);
           
這裡當wait函數被調用時,2個event都處于未通知狀态,這就迫使調用者進入等待狀态。直到2個event都變為已通知,否則調用線程會一直等待下去。
// psa:事件的安全屬性
// bManualReset:是否手動重置,TRUE手動重置,否則為自動
// fInitialState:初始狀态,TRUE為有信号,FALSE為無信号,一般初始化為FALSE
HANDLE CreateEvent(PSECURITY_ATTRIBUTES psa,
                  BOOL bManualReset,
                  BOOL fInitialState,
                  PCTSTR pszName);

// 打開pszName名字的事件,成功傳回handle失敗傳回NULL
HANDLE OpenEvent(DWORD fdwAccess,BOOL bInherit,PCTSTR pszName);

// 設定事件為通知狀态
HANDLE SetEvent(HANDLE hEvent);

// 将事件重置為未通知狀态
BOOL ResetEvent(HANDLE hEvent);

// PulseEvent函數使得事件變為已通知狀态,然後立即又變為未通知狀态,這就像在調用SetEvent後又立即調用ResetEvent函數一樣。
// 并不非常有用。實際上我在自己的應用程式中從未使用它,因為根本不知道什麼線程将會看到事件的發出并變成可排程線程。
BOOL PulseEvent(HANDLE hEvent);
           

可等待的定時器核心對象線程同步

// 建立一個可等待的定時器核心對象,并傳回其句柄
HANDLE CreateWaitableTimer(PSECURITY_ATTRIBUTES psa,BOOL bManual,PCTSTR pszName);

// 打開指定名稱的可等待定時器
HANDLE OpenWaitableTimer(DWORD dwDesiredAccess,BOOL bInheritHandle,PCTSTR pszName);

// 設定可等待定時器
// pDueTime:指明定時器合适第一次報時,
// lPeriod:第一次報時之後間隔多長時間報一次時,機關為秒,如果為0則隻報時一次
// pfnCompltetionRoutine:回調函數
// pvargToCompletionRoutine:回調參數
// fResume:暫停或恢複,一般為FALSE
BOOL SetWaitableTimer(HANDLE hTimer,const LARGE_INTERGER *pDueTime,LONG lPeriod,PTIMERAPCROUTINE pfnCompltetionRoutine,PVOID pvargToCompletionRoutine,BOOL fResume);

// 取出定時器的句柄并撤銷定時器
BOOL CancelWaitableTimer(HANDLE hTimer);
           

等待定時器給APC(異步過程調用)進行排隊

一般而言pfnCompltetionRoutine和其參數都為null,當規定的時間到來時就想定時器發送通知信号。但是如果setwaitabletimer的pfnCompltetionRoutine參數不為null,則當定時器報時的時候,由調用setwaitabletimer函數的同一線程來調用,但隻有在調用線程處于等待狀态(sleep、waitforsingle…、waitformulti、msgwaitfor等函數)的時候才會被調用,否則不會被調用。

當定時器報時的時候,如果你的線程處于等待狀态,則系統使你的線程調用回調函數。

定時器的松散特性

定時器通常用于通信協定中。比如,如果可以寄向伺服器發出一個請求,而伺服器沒有在規定的事件内做出響應,那麼客戶機就認為無法使用伺服器。如果發現自己需要建立和管理多個定時器對象,可以使用createtimerqueuetimer,但是IO完成端口才是最完美的選擇。

信号量線程同步

信号量用于對資源進行計數,他可以很好的管理資源和排程線程。
// lInitialCount:初始資源數
// lMaxCount:最大資源數
HANDLE CreateSemophere(PSECURITY_ATTRIBUTE psa,LONG lInitialCount,LONG lMaxCount,PCTSTR pszName);

HANDLE OpenSemophere(DWORD fdwAccess,BOOL bInheritHandle,PCTSTR pszName);

// 釋放信号量,也就是将資源數進行遞增lReleaseCount個個數
// plPrevCount:傳回目前資源數的原始值,一般傳遞NULL即可
BOOL ReleaseSemaphore(HANDLE hSem,LONG lReleaseCount,PLONG plPrevCount);
           
// 建立一個信号量,其最大資源數為5,開始時可以使用的資源為0
HANDLE hSem = CreateSemophere(NULL,0,5,NULL);
           

如果資源數量初始化為0,是以不發出信号量的信号,等待信号量的所有線程都進入等待。信号量的出色之處在于它們能夠以原子操作方式來執行測試和設定操作,這就是說,當向信号量申請一個資源時,作業系統就要檢查是否有這個資源可供使用,同時将可用資源的數量遞減,而不讓另一個線程加以幹擾。隻有當資源數量遞

減後,系統才允許另一個線程申請對資源的通路權。

如果一個線程調用waitfor函數對semaphore進行等待,并且信号量的目前資源數為0(信号量沒有發出信号),那麼系統就進入等待狀态。當另一個線程将該信号量的目前資源數量進行遞增的時候,系統就會通知等待在該信号量上的線程。

互斥體線程同步

互斥體一般用于保護多線程中對共享資源的通路。
// fInitialOwner:指明互斥體的初始狀态,一般初始為FALSE
HANDLE CreateMutex(PSECURITY_ATTRIBUTE psa,BOOL fInitialOwner,PCTSTR pszName);

HANDLE OpenMutex(DWORD fdwAccess,BOOL bInherit,PCTSTR pszName);

// 當一個線程成功等待到一個互斥體,然後執行完必要的操作後,需要調用release函數釋放對互斥體的占有
BOOL RealseMutex(HANDLE hMutex);
           
如果目前占有互斥體的線程異常結束,則系統自動對該線程的占用進行release,然後其他wait的線程将傳回WAIT_ABANDONED值,表示上個擁有者放棄了擁有權,一般是由于該線程在完成對共享資源的使用前終止運作。

互斥體與臨界區比較

互斥體和臨界區都可以保護資源在多線程中的通路,他們存在以下主要差別:
特性 互斥體 臨界區
運作速度
是否可以跨程序
聲明 HANDLE hMutex CRITICAL_SECTION cs;
初始化 hMutex = CreateMutex() InitializeCriticalSection(&cs)
清除 ReleaseMutex(hMutex) DeleteCriticalSection(&cs)
等待資源 WaitForSingleObject EnterCriticalSection
釋放 ReleaseMutex(hMutex) LeaveCriticalSection(&cs)

繼續閱讀