天天看點

臨界區和C++使用方式

一.臨界資源

        臨界資源是一次僅允許一個程序使用的共享資源。各程序采取互斥的方式,實作共享的資源稱作臨界資源。屬于臨界資源的硬體有,列印機,錄音帶機等;軟體有消息隊列,變量,數組,緩沖區等。諸程序間采取互斥方式,實作對這種資源的共享。

二.臨界區:

        每個程序中通路臨界資源的那段代碼稱為臨界區(criticalsection),每次隻允許一個程序進入臨界區,進入後,不允許其他程序進入。不論是硬體臨界資源還是軟體臨界資源,多個程序必須互斥的對它進行通路。多個程序涉及到同一個臨界資源的的臨界區稱為相關臨界區。使用臨界區時,一般不允許其運作時間過長,隻要運作在臨界區的線程還沒有離開,其他所有進入此臨界區的線程都會被挂起而進入等待狀态,并在一定程度上影響程式的運作性能。

三、優缺點

優點:效率高,與互斥和事件這些核心同步對象相比,臨界區是使用者态下的對象,即隻能在同一程序中實作線程互斥。因無需在使用者态和核心态之間切換,是以工作效率比較互斥來說要高很多。

缺點:資源釋放容易出問題,Critical Section不是一個核心對象,無法獲知進入臨界區的線程是生是死,如果進入臨界區的線程挂了,沒有釋放臨界資源,系統無法獲知,而且沒有辦法釋放該臨界資源。

臨界區是一種輕量級的同步機制,與互斥和事件這些核心同步對象相比,臨界區是使用者态下的對象,

即隻能在同一程序中實作線程互斥。因無需在使用者态和核心态之間切換,是以工作效率比較互斥來說要高很多。

四、API介紹

1、初始化對象

InitializeCriticalSection

2、嘗試進入臨界區,如果成功調用,則進入臨界區,如果資源被占用,不會阻塞

​​TryEnterCriticalSection​​​This function attempts to enter a critical section without blocking. If the call is successful, the calling thread takes ownership of the critical section.

A nonzero value indicates that the critical section is successfully entered or the current thread already owns the critical section. Zero (FALSE) indicates 

that another thread already owns the critical section.

3、EnterCriticalSection  進入臨界資源,取到控制權之前會阻塞

 Before using a critical section, some thread of the process must call the InitializeCriticalSection to initialize the object.

 To enable mutually exclusive access to a shared resource, each thread calls the EnterCriticalSection function to request ownership of the critical section before executing any section of code that accesses the protected resource。

 EnterCriticalSection blocks until the thread can take ownership of the critical section. 

4、離開臨界資源 

 When it has finished executing the protected code, the thread uses the LeaveCriticalSection function

 to relinquish ownership, enabling another thread to become owner and access the protected resource. 

 The thread must call LeaveCriticalSection once for each time that it entered the critical section.

5、删除臨界對象

Any thread of the process can use the DeleteCriticalSection function to release the system resources that were allocated when the critical section object was initialized. After this function has been called, the critical section object can no longer be used for synchronization.

删除之後臨界資源就無效了

五、代碼示範

#include <iostream>
#include <windows.h>
#include <thread>
using namespace std;
CRITICAL_SECTION g_cs;

int g_Count = 0;
void func1()
{
  while(1)
  {
    EnterCriticalSection(&g_cs);
    g_Count++;
    cout <<"t1 g_Count = " << g_Count << endl;
    Sleep(3000);
    LeaveCriticalSection(&g_cs);
  }
}

void func2()
{
  while (1)
  {
    EnterCriticalSection(&g_cs);
    g_Count++;
    cout << "t2 g_Count = " << g_Count << endl;

    Sleep(2000);
    LeaveCriticalSection(&g_cs);
  }
}
int main()
{
  InitializeCriticalSection(&g_cs);
  std::thread t1(func1); // t1 is not a thread
  std::thread t2(func2); // t1 is not a thread
  t1.join();
  t2.join();

  
  cin.get();
  DeleteCriticalSection(&g_cs);
  return 0;
}      

六、注意

1. 臨界區對象不是核心對象,是以不能繼承,不能跨程序,也不能用waitfor什麼的函數來限定時間等待。這個很好了解,你想想WaitFor要求傳一個句柄,而臨界區對象的類型都不是句柄,也不能用CloseHandle來關閉,怎麼可能會能讓WaitForXXX搞了。

2. 臨界區對象使用前必須初始化,不初始化會崩潰,這是我的親曆。

3. 線程進入臨界區之前會先自旋那麼幾次,所有自旋鎖都失敗了之後會建立一個核心對象然後等待這個核心進而進入到核心模式。

4. Enter和Leave必須比對,每Enter一次,就得Leave一次,這又是可惡的計數機制。參見下面的​​代碼​​:

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;      

這是臨界區對象的定義,看見

RecursionCount      

這個對象了吧,你覺得它能幹點啥?同時在這裡你還能看到一個信号量的核心對象,還有一個自旋數量。這些玩意印證了上面的話。如果你同一個線程Leave之前Enter了兩次,必須調用兩個Leave,不然這個臨界區對象依然會阻塞别的線程。再不明白就去看我前面有關挂起線程的那個博文。

5. 由于進入臨界去是無限等待的,是以你有時間肯定希望有種方法能夠檢視一下臨界區是否可用,不可用則希望線程立刻去做其它的事情。這時候,你就需要一個TryEnterCriticalSectionAPI,這玩意很好了解,你踹一腳臨界區,如果能進去就進去,不能進去這個API立刻以False傳回,你就可以安排線程去做其它的事情。注意:你一腳踹進去了之後完事了記得要離開(LeaveCriticalSection)。

5. 由于進入臨界去是無限等待的,是以你有時間肯定希望有種方法能夠檢視一下臨界區是否可用,不可用則希望線程立刻去做其它的事情。這時候,你就需要一個TryEnterCriticalSectionAPI,這玩意很好了解,你踹一腳臨界區,如果能進去就進去,不能進去這個API立刻以False傳回,你就可以安排線程去做其它的事情。注意:你一腳踹進去了之後完事了記得要離開(LeaveCriticalSection)。

6. 前面說了,臨界區真正用核心對象挂起線程之前會自旋好幾次,是以你看對象裡就有一個自旋鎖的計數。你可以改這個自旋鎖的數量。當然我不是說讓你直接修改對象的成員變量!你可以在初始化的時候指定自旋鎖的數量,用這個API:InitializeCriticalSectionAndSpinCount。在這裡小說一下臨界區為什麼會自旋。因為​​程式​​從使用者态轉到核心模式需要昂貴的開銷(大概數百個CPU周期),很多情況下,A線程還沒完成從使用者态轉到核心态的操作呢,B線程就已經釋放資源了。于是臨界區就先隔一段時間自旋一次,直到所有自旋次數都耗盡,就建立個核心對象然後挂起線程。但是,如果您的機器隻有一個CPU,那麼這個自旋次數就沒用了,作業系統直接會無視它。原因如下:你自旋着呢,操作B線程釋放不了資源,于是你還不如直接切入等待狀态讓B來釋放資源。動态更改自旋數量請使用SetCriticalSectionSpinCount,别做直接更改對象成員變量的二事!

7. 最後,初始化臨界區和進入臨界區的時候都有可能會遇到異常狀況,比如初始化的時候需要申請一些記憶體來儲存DEBUG的資訊(參見上面代碼的第一個成員變量),如果記憶體不夠,初始化就崩了。進入臨界區的時候可能需要建立一個核心對象,如果記憶體不夠,也崩了。解決這個問題的方法有兩種

  1. 結構化異常處理
  2. 初始化的時候使用InitializeCriticalSectionAndSpinCount。這個API有傳回值,如果它申請DEBUG資訊失敗,傳回FALSE,另外,剛才提到了這個API可以指定自旋鎖的自旋次數。這個自旋次數的範圍并非是用0到0xFFFF

    FFFF而是0--->0x00FF FFFF,是以你可以設定高位為1訓示初始化的時候直接建立核心對象。如果建立失敗,這個函數也會調用失敗。當然了,一上來就搞一個核心對象似乎有點浪費記憶體,但是這樣能夠保證進入臨界區不會失敗!但是吧,你需要注意,設定高位來保證核心對象的建立隻能在2000上玩。MSDN上有說明,不信你看:

  3. Windows  2000:  If the high-order bit is set, the function pre-allocates the event used by the​​EnterCriticalSection​​​ function. Pre-allocation guarantees that entering or leaving the critical section will not raise an exception in low memory conditions. Do not set this bit if you are creating a large number of critical section objects, because it consumes a significant amount

    of nonpaged pool. Note that this event is allocated on demand starting with Windows XP and the high-order bit is ignored.

最後是一些實驗:

我們看看用InitializeCriticalSection初始化一個臨界區對象後,這些成員變量(除去DEBUG外)都是什麼樣子。

臨界區和C++使用方式

我們Enter一下,看看會變成什麼樣子

臨界區和C++使用方式

我們再讓其它線程也Enter一下看看

臨界區和C++使用方式

可見,建立了一個核心對象。

我們現在讓主線程退出臨界區

臨界區和C++使用方式

對照線程句柄我們可以看出第二個線程獲得了臨界區對象。

我們再讓第二個線程退出臨界區。

臨界區和C++使用方式

臨界區除去核心對象外回到了原始狀态。

實驗2:我們讓臨界區對象在同一線程内相繼被進入兩次

::EnterCriticalSection(&g_cs);
::EnterCriticalSection(&g_cs);      
臨界區和C++使用方式

可見,計數增加了一個,變成了,是以你得leave兩次才能開鎖

RTL_CRITICAL_SECTION 結構。為友善起見,将此結構列出如下:

struct RTL_CRITICAL_SECTION
{
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread;
HANDLE LockSemaphore;
ULONG_PTR SpinCount;
};      

以下各段對每個字段進行說明。

DebugInfo 此字段包含一個指針,指向系統配置設定的伴随結構,該結構的類型為 

RTL_CRITICAL_SECTION_DEBUG。這一結構中包含更多極有價值的資訊,也定義于 WINNT.H 中。我們稍後将對其進行更深入地研究。

LockCount 這是臨界區中最重要的一個字段。它被初始化為數值 -1;此數值小于-1 

時,表示此臨界區被占用。當其不等于 -1 時,OwningThread 字段(此字段被錯誤地定義于 WINNT.H 中 — 應當是 DWORD 而不是 

HANDLE)包含了擁有此臨界區的線程 ID。

RecursionCount 

此字段包含目前所有者線程已經獲得該臨界區的次數。如果該數值為零,下一個嘗試擷取該臨界區的線程将會成功。

OwningThread 此字段包含目前占用此臨界區的線程的線程辨別符。此線程 ID 與 

GetCurrentThreadId 之類的 API 所傳回的 ID 相同。

LockSemaphore 

此字段的命名不恰當,它實際上是一個自複位事件,而不是一個信号。它是一個核心對象句柄,用于通知作業系統:該臨界區現在空閑。作業系統在一個線程第一次嘗試獲得該臨界區,但被另一個已經擁有該臨界區的線程所阻止時,自動建立這樣一個句柄。應當調用 

DeleteCriticalSection(它将發出一個調用該事件的 CloseHandle 調用,并在必要時釋放該調試結構),否則将會發生資源洩漏。

SpinCount 旋轉次數。僅用于多處理器系統。MSDN 

文檔對此字段進行如下說明:“在多處理器系統中,如果該臨界區不可用,調用線程将在對與該臨界區相關的信号執行等待操作之前,旋轉 dwSpinCount 

次。如果該臨界區在旋轉操作期間變為可用,該調用線程就避免了等待操作。”旋轉計數可以在多處理器計算機上提供更佳性能,其原因在于在一個循環中旋轉通常要快于進入核心模式等待狀态。此字段預設值為零,但可以用 

InitializeCriticalSectionAndSpinCount API 将其設定為一個不同值。

InitializeCriticalSectionAndSpinCount作用

The InitializeCriticalSectionAndSpinCount function initializes a critical section object and sets the spin count for the critical section.

BOOL InitializeCriticalSectionAndSpinCount(

  LPCRITICAL_SECTION lpCriticalSection,

                      // pointer to critical section

  DWORD dwSpinCount   // spin count for critical section

);SetCriticalSectionSpinCountThe SetCriticalSectionSpinCount function sets the spin count for the specified critical section. DWORD SetCriticalSectionSpinCount(

  LPCRITICAL_SECTION lpCriticalSection, 

                      // pointer to critical section

  DWORD dwSpinCount   // spin count for critical section

);

當線程試圖進入另一個線程擁有的關鍵代碼段時,調用線程就立即被置于等待狀态。這意

味着該線程必須從使用者方式轉入核心方式(大約1 0 0 0個C P U周期)。這種轉換是要付出很大代價的。

是以, InitializeCriticalSectionAndSpinCount 的作用不同于InitializeCriticalSection 之處就在于設定了一個循環鎖,不至于使線程立刻被置于等待狀态而耗費大量的CPU周期,而在dwSpinCount後才轉為核心方式進入等待狀态。通常dwSpinCount設為4000較為合适 。 

實際上對 CRITICAL_SECTION 的操作非常輕量,為什麼還要加上旋轉鎖的動作呢?其實這個函數在單cpu的電腦上是不起作用的,隻有當電腦上存在不止一個cpu,或者一個cpu但多核的時候,才管用。

如果臨界區用來保護的操作耗時非常短暫,比如就是保護一個reference counter,或者某一個flag,那麼幾個時鐘周期以後就會離開臨界區。可是當這個thread還沒有離開臨界區之前,另外一個thread試圖進入 此臨界區——這種情況隻會發生在多核或者smp的系統上——發現無法進入,于是這個thread會進入睡眠,然後會發生一次上下文切換。我們知道context switch是一個比較耗時的操作,據說需要數千個時鐘周期,那麼其實我們隻要再等多幾個時鐘周期就能夠進入臨界區,現在卻多了數千個時鐘周期的開銷,真 是是可忍孰不可忍。

是以就引入了InitializeCriticalSectionAndSpinCount函數,它的第一個參數是指向cs的指針,第二個參數 是旋轉的次數。我的了解就是一個循環次數,比如說N,那麼就是說此時EnterCriticalSection()函數會内部循環判斷此臨界區是否可以進 入,直到可以進入或者N次滿。我們增加的開銷是最多N次循環,我們可能獲得的紅利是數千個時鐘周期。對于臨界區内很短的操作來講,這樣做的好處是大大的。

MSDN上說,他們對于堆管理器使用了N=4000的旋轉鎖,然後“This gives great performance and scalability in almost all worst-case scenarios.” 可見還是很有用的:-)

8、多次調用 LeaveCriticalSection 導緻死鎖

繼續閱讀