本系列意在記錄Windwos線程的相關知識點,包括線程基礎、線程調度、線程同步、TLS、線程池等。
關鍵段
關鍵段(Critical Section)是一小段代碼,它在執行之前需要獨佔對一些共用資源的訪問權。這種方式可以讓多行代碼以“原子方式”對資源進行操控。這裡的原子方式,指的是代碼知道除了當前線程之外,沒有其他任何線程會同時訪問該資源。當然,系統仍然可以暫停當前線程去調度其他線程。但是,在當前線程離開關鍵段之前,系統是不會去調度任何想要訪問同一資源的其他線程的。
下面的代碼展示了Critical Section的使用方法:
const int COUNT = 10;int g_nSum = 0;CRITICAL_SECTION g_cs;//CRITICAL_SECTION structDWORD WINAPI FirstThread(PVOID pvParam){EnterCriticalSection(&g_cs);//Try enter critical sectiong_nSum = 0;for(int n = 1 ; n <= COUNT ; n++) g_nSum+=n;LeaveCriticalSection(&g_cs);return(g_nSum);}DWORD WINAPI SecondThread(PVOID pvParam){EnterCriticalSection(&g_cs);//Try enter critical sectiong_nSum = 0;for(int n = 1 ; n <= COUNT ; n++) g_nSum+=n;LeaveCriticalSection(&g_cs);return(g_nSum);}
假如沒有上面的EnterCriticalSection和LeaveCriticalSection,當兩個線程函數分別在兩個線程中執行的時候,g_nSum的狀態是不可預計的。
在上面的代碼中,首先定義了一個叫g_cs的CRITICAL_SECTION資料結構,然後把任何需要訪問共用資源(這裡的g_nSum)的代碼放在EnterCriticalSection和LeaveCriticalSection之間。這裡需要注意的是,關鍵段需要用在所有的相關線程中(即:上面的兩個線程函數都要放在關鍵段中),否則共用資源還是有可能被破壞(只要對線程調度有清晰的認識就很容易理解其中的原因)。另外,在調用EnterCriticalSection之前需要調用InitializeCriticalSection初始化,當不需要訪問共用資源的時候,應該調用DeleteCriticalSection:
/* Sample C/C++, Windows, link to kernel32.dll */#include <windows.h> static CRITICAL_SECTION cs; /* This is the critical section object -- once initialized, it cannot be moved in memory */ /* If you program in OOP, declare this as a non-static member in your class */ /* Initialize the critical section before entering multi-threaded context. */InitializeCriticalSection(&cs); void f(){ /* Enter the critical section -- other threads are locked out */ EnterCriticalSection(&cs); /* Do some thread-safe processing! */ /* Leave the critical section -- other threads can now EnterCriticalSection() */ LeaveCriticalSection(&cs);} /* Release system object when all finished -- usually at the end of the cleanup code */DeleteCriticalSection(&cs);
關鍵段工作原理
EnterCriticalSection會檢查CRITICAL_SECTION中某些成員變數,這些成員變數表示是否有線程正在訪問資源:
- 如果沒有線程正在訪問資源,那麼EnterCriticalSection會更新成員變數,以表示調用線程已經獲准對資源的訪問,並立即返回,這樣線程就可以繼續執行。
- 如果成員變數表示調用線程已經獲准訪問資源,那麼EnterCriticalSection會更新變數,以表示調用線程被獲准訪問的次數。
- 如果成員變數表示其他線程已經獲准訪問資源,那麼EnterCriticalSection會使用一個事件核心對象把當前線程切換到等待狀態。這樣線程不會像前一篇講的旋轉鎖(spinlock)那樣耗費CPU。
關鍵段的核心價值在於它能夠以原子的方式執行所有這些測試。另外TryEnterCriticalSection跟EnterCriticalSection一樣擁有對共用資源的檢測能力,但是不會阻塞調用線程。
關鍵段與旋轉鎖
關鍵段的另一個核心價值在於它可以使用旋轉鎖來對共用資源進行一定時間的“爭用”,而不是立刻讓線程進入等待狀態、進入核心模式(線程從使用者模式切換到核心模式大約需要1000個CPU周期)。因為,很多情況下共用資源不太會佔用太長的時間,如果因為一個即將釋放的共用資源而將線程切換到核心模式,將得不償失。所以預設情況下在關鍵段阻塞線程之前,會多次嘗試用旋轉鎖來“爭用”共用資源,如果在這期間“爭用”成功,那麼EnterCriticalSection就會返回,代碼將進入關鍵段執行;如果沒有成功,則會將線程切換到等待狀態。需要注意的是:只有在多核情況下才能夠使關鍵段嘗試這種特性。
為了在使用關鍵段的時候同時使用旋轉鎖,必須用如下函數來初始化關鍵段:
BOOL WINAPI InitializeCriticalSectionAndSpinCount( __out LPCRITICAL_SECTION lpCriticalSection, __in DWORD dwSpinCount);
下面的函數用以改變關鍵段的旋轉次數:
DWORD WINAPI SetCriticalSectionSpinCount( __inout LPCRITICAL_SECTION lpCriticalSection, __in DWORD dwSpinCount);
關鍵段還可以和條件變數配合使用,這部分內容將在下一篇涉及。
更多關於關鍵段的內容可以參見:http://blog.csdn.net/morewindows/article/details/7442639
最後,設計一個簡單的帶一個緩衝隊列的Log方法,要求安全執行緒,下面給出C++的實現:
void Log(int nLevel, const WCHAR* message){struct DelayedLogInfo{int level;std::wstring message;};static std::list<DelayedLogInfo> c_LogDelay; //log記錄的緩衝隊列if (TryEnterCriticalSection(&g_CsLog)) //獲得整個log的存取權限,如果失敗則嘗試在else裡面獲得對隊列的存取權限{EnterCriticalSection(&g_CsLogDelay);//讀隊列前,獲得表示”隊列“的變數的存取權限while (!c_LogDelay.empty())//迴圈把隊列中的東西全都寫掉{DelayedLogInfo& logInfo = c_LogDelay.front();LogInternal(logInfo.level, logInfo.message.c_str());c_LogDelay.erase(c_LogDelay.begin());}LeaveCriticalSection(&g_CsLogDelay);//釋放表示”隊列“的變數的存取權限//代碼到這裡釋放了隊列這個共用對象,因此,在下面這真正寫入log時,其他試圖寫log的線程將只能向緩衝隊列中寫資料// Log the messageLogInternal(nLevel, message);LeaveCriticalSection(&g_CsLog);}else{EnterCriticalSection(&g_CsLogDelay); //寫隊列前,獲得表示”隊列“的變數的存取權限DelayedLogInfo logInfo = {nLevel, message};c_LogDelay.push_back(logInfo);//寫隊列LeaveCriticalSection(&g_CsLogDelay);//釋放表示”隊列“的變數的存取權限}}
勞動果實,轉載請註明出處:http://www.cnblogs.com/P_Chou/archive/2012/06/20/critical-section-in-thread-sync.html