【Windows】線程漫談——線程同步之關鍵段


本系列意在記錄Windwos線程的相關知識點,包括線程基礎、線程調度、線程同步、TLS、線程池等。

 

關鍵段

關鍵段(Critical Section)是一小段代碼,它在執行之前需要獨占對一些共享資源的訪問權。這種方式可以讓多行代碼以“原子方式”對資源進行操控。這里的原子方式,指的是代碼知道除了當前線程之外,沒有其他任何線程會同時訪問該資源。當然,系統仍然可以暫停當前線程去調度其他線程。但是,在當前線程離開關鍵段之前,系統是不會去調度任何想要訪問同一資源的其他線程的。

下面的代碼展示了Critical Section的使用方法:

const int COUNT = 10;
int g_nSum = 0;
CRITICAL_SECTION g_cs;//CRITICAL_SECTION struct

DWORD WINAPI FirstThread(PVOID pvParam){
	EnterCriticalSection(&g_cs);//Try enter critical section
	g_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 section
	g_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)的代碼放在EnterCriticalSectionLeaveCriticalSection之間。這里需要注意的是,關鍵段需要用在所有的相關線程中(即:上面的兩個線程函數都要放在關鍵段中),否則共享資源還是有可能被破壞(只要對線程調度有清晰的認識就很容易理解其中的原因)。另外,在調用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 message
		LogInternal(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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM