在多線程環境下,線程同步是不可避免的話題。Windows環境下的線程同步分為:用戶態同步 與 內核態同步。
下面我們先了解用戶態同步的一些方法。
- 使用Interlocked系列函數。簡單易用的函數。
- 關鍵段。用來對關鍵資源實現獨享訪問。
- Slim讀寫鎖。靈活的進行寫獨享讀共享操作。
- 條件變量。當線程要進行較為復雜的條件進行同步時,可以實現。
Interlocked系列函數。
Windows提供了Interlocked系列函數,用來原子的對數據進行增減或交換。如自旋轉鎖,就可以通過InterlockedExchange函數實現。
~~~
BOOL g_fResourceInUse = FALSE;
void Func1()
{
// InterlockedExchange會一直原子設置g_fResourceInUse為TRUE,同時返回g_fResourceInUse
// 上一次的值。當g_fResourceInUse為初始值FALSE或被另一線程設置為FALSE后,該循環才會結束。
// 以此自循環(自旋鎖)方式來實現對資源的獨占訪問。
while (InterlockedExchange(&g_fResourceInUse,TRUE) == TRUE)
Sleep(0);
// Access the resource, do something
...
// Do not need the resource anymore, release it
InterlockedExchange(&g_fResourceInUse,FALSE)
}
~~~
注意,這種循環方式會占用CPU大量時間,不建議在單CPU機器上運行。(可以用關鍵段代替或用C++11標準中的atom系列函數代替)
關鍵段
上面使用自旋鎖的方式進行同步顯然是低效的。因為等待線程依然處于可調度狀態,仍然會占用CPU時間。Windows提供了一系列函數,讓線程同步。這一系列函數保證了在線程獲得想要的資源之前,不被CPU調度,直到其要求的資源可被線程訪問為止。關鍵段就是其一,其實關鍵段的實現是通過事件內核對象的。
運用關鍵段五個步驟:
1、聲明一個可以被多個線程訪問到其地址的關鍵段變量。
2、在使用關鍵段前,調用InitializeCriticalSection函數初始化關鍵段。
2、在進入資源前調用EnterCriticalSection,請求進入關鍵段(若進入不了,則線程等待)
3、在離開資源時,調用LeaveCriticalSection,離開關鍵段。
若確定了關鍵段已經不被任何線程再使用,則要銷毀關鍵段對象。
4、在不再使用關鍵段時,調用DeleteCriticalSection銷毀關鍵段。
~~~
CRITICAL_SECTION g_cs;
int g_sum = 0;
//初始化關鍵段,注意不要多次初始化,否則后果是未定義的
<pre name="code" class="cpp">InitializeCriticalSection(&g_cs);
~~~
~~~
void ThreadFunc1()
{
??
?? EnterCriticalSection(&g_cs);
?? g_sum++;
?? LeaveCriticalSection(&g_cs);
}
void ThreadFunc2()
{
??
?? EnterCriticalSection(&g_cs);
?? g_sum++;
?? LeaveCriticalSection(&g_cs);
}
...
...
// 不再使用critical section,顯示銷毀
DeleteCriticalSection(&g_cs);
~~~
關鍵段最容易忘記
~~~
LeaveCriticalSection
~~~
,這時候可以用RAII技巧來進行簡單的封裝。
關于關鍵段的細節
1、若一個線程已經成功進入關鍵段,則可以多次調用EnterCriticalSection,相應的,要調用多次LeaveCriticalSection來離開臨界區。
2、對于跨進程的線程同步,可以使用mutex對象。
可以使用TryEnterCriticalSection進入關鍵段,他不會使線程進入等待,而是返回布爾值表示是否獲得了關鍵段。對于返回TRUE,需要調用LeaveCriticalSection。
關鍵段與旋轉鎖
當線程由于得不到關鍵段而進入等待狀態時,會進行用戶態和內核態切換,這會占用大量的CPU時間。在多處理器的環境下,可能的一種情況是,用戶/內核態的切換還未結束,占用關鍵段的線程可能已經釋放了關鍵段。
在多處理器的情況下,可以使用
[**InitializeCriticalSectionAndSpinCount**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms683476%28v=vs.85%29.aspx)
函數來初始化關鍵段。其函數原型如下
~~~
BOOL WINAPI InitializeCriticalSectionAndSpinCount(
_Out_ LPCRITICAL_SECTION lpCriticalSection,
_In_ DWORD dwSpinCount
);
~~~
其中參數dwSpinCout用來設置旋轉鎖循環次數。[**SetCriticalSectionSpinCount**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms686197%28v=vs.85%29.aspx) 可以重設自旋轉鎖次數。
該函數會在進入內核態前,旋轉設置的循環次數來獲取關鍵段。若在旋轉鎖階段獲取關鍵段,則不會進入內核態。
注意,在單CPU模式下,dwSpinCout是被忽略的,總是為0。因為在單CPU下,
~~~
InitializeCriticalSectionAndSpinCount
~~~
是沒有意義的:CPU在旋轉鎖階段被線程占用,其他線程根本沒有時機來釋放關鍵段。但我們仍可以這樣初始化關鍵段,以應對未來可能的多CPU環境。
Slim讀寫鎖
一般的,對于線程的同步,讀是可以共享的,而寫則是互斥的。因此Windows提供了讀寫鎖機制。
與關鍵段類似,在使用讀寫鎖之前,要調用[**InitializeSRWLock**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms683483%28v=vs.85%29.aspx)函數初始化讀寫鎖。
利用讀寫鎖要分清讀者和寫者。
讀寫鎖使用步驟
1、聲明SRWLOCK對象。
2、用InitializeSRWLock函數初始化SRWLOCK對象。
3.1、對于讀者,調用
[**AcquireSRWLockShared**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms681934%28v=vs.85%29.aspx)
[**ReleaseSRWLockShared**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms685080%28v=vs.85%29.aspx)
以共享的方式獲取,釋放的讀寫鎖。若該鎖沒被占用或被其他線程讀,則立即獲得鎖,否則等待。
3.2、對于寫者,調用
[**AcquireSRWLockExclusive**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms681930%28v=vs.85%29.aspx)
[**ReleaseSRWLockExclusive**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms685076%28v=vs.85%29.aspx)
以獨占的方式獲取,釋放的讀寫鎖。若該鎖未被占用,則立即獲得鎖,否則等待。
Slim與關鍵段的對比
Slim鎖與關鍵段主要有以下兩點區別
1、Slim鎖不能夠遞歸獲取,即當一個線程Acquire并獲得Slim鎖之后,不能夠再次Acquire同一把鎖。
2、不存在TryEnter類似函數獲取Slim鎖。
3、Slim鎖不用顯示銷毀,系統會自動釋放。
4、總體上說,Slim鎖的效率優于關鍵段。
多種同步方法的效率對比

條件變量同步
有時候需要線程原子方式釋放獲得的鎖同時阻塞自身,直到某一條件成立為止。這時候可以通過條件變量進行同步。
等待條件變量函數。當條件被滿足,線程被喚醒后,會自動得到鎖。
[**SleepConditionVariableCS**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms686301%28v=vs.85%29.aspx)
[**SleepConditionVariableSRW**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms686304%28v=vs.85%29.aspx)
喚醒等待條件的線程函數
[**WakeAllConditionVariable**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687076%28v=vs.85%29.aspx)
[**WakeConditionVariable**](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687080%28v=vs.85%29.aspx)