C#多執行緒同步技術

方寸之間發表於2011-06-25

     我們可以在計算機上執行各種計算機軟體程式。每一個執行的程式可能包括多個獨立執行的執行緒(Thread)。
     執行緒(Thread)是一份獨立執行的程式,有自己專用的執行棧。執行緒有可能和其他執行緒共享一些資源,比如,記憶體,檔案,資料庫等。
     當多個執行緒同時讀寫同一份共享資源的時候,可能會引起衝突。這時候,我們需要引入執行緒“同步”機制,即各位執行緒之間要有個先來後到,不能一窩蜂擠上去搶作一團。
     同步這個詞是從英文synchronize(使同時發生)翻譯過來的。我也不明白為什麼要用這個很容易引起誤解的詞。既然大家都這麼用,我們們也就只好這麼將就。
     執行緒同步的真實意思和字面意思恰好相反。執行緒同步的真實意思,其實是“排隊”:幾個執行緒之間要排隊,一個一個對共享資源進行操作,而不是同時進行操作。 
     1)執行緒同步就是執行緒排隊。同步就是排隊。執行緒同步的目的就是避免執行緒“同步”執行。這可真是個無聊的繞口令。 
     2)只有共享資源的讀寫訪問才需要同步。如果不是共享資源,那麼就根本沒有同步的必要。
     3)只有“變數”才需要同步訪問。如果共享的資源是固定不變的,那麼就相當於“常量”,執行緒同時讀取常量也不需要同步。至少一個執行緒修改共享資源,這樣的情況下,執行緒之間就需要同步。
     4)多個執行緒訪問共享資源的程式碼有可能是同一份程式碼,也有可能是不同的程式碼;無論是否執行同一份程式碼,只要這些執行緒的程式碼訪問同一份可變的共享資源,這些執行緒之間就需要同步。


 

C#.Net提供了三種方法來完成對共享資源 ,諸如全域性變數域,特定的程式碼段,靜態的和例項化的方法和域。

    1 程式碼域同步:使用Monitor類可以同步靜態/例項化的方法的全部程式碼或者部分程式碼段。不支援靜態域的同步。在例項化的方法中,this指標用於同步;而在靜態的方法中,類用於同步。

    2 手工同步:使用不同的同步類(諸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent Interlocked等)建立自己的同步機制。這種同步方式要求你自己手動的為不同的域和方法同步,這種同步方式也可以用於程式間的同步和對共享資源的等待而造成的死鎖解除。

    3 上下文同步:使用SynchronizationAttributeContextBoundObject物件建立簡單的,自動的同步。這種同步方式僅用於例項化的方法和域的同步。所有在同一個上下文域的物件共享同一個鎖。

 

1、lock語句和執行緒安全

     lock語句是設定鎖定和解除鎖定的一種簡單方式。用lock語句定義的物件表示,要等待指定物件的鎖定解除。只能傳送引用型別,鎖定值型別只是鎖定了一個副本,這是沒有什麼意義的。編譯器會提供一個鎖定值型別的錯誤。進行了鎖定後----只有一個執行緒得到了鎖定塊,就可以執行lock語句塊。在lock語句塊的最後,物件的鎖定解除,另一個等待鎖定的執行緒就可以獲得該鎖定塊。

   使用lock語句的兩種形式:

 lock (obj)            
{                
    // 同步區域            
}            
lock (typeof(StaticClass))            
{            
}

    使用lock關鍵字可以使類的例項成員設定為執行緒安全。這樣只有一個執行緒能訪問該例項的方法。如:

		public class Demo        
		{            
			public void DoThis()            
			{                
				lock(this)                
				{                    
					// ...                
				}            
			}        
		}

    但是例項的物件也可以用於外部的同步訪問,我們不在在類中控制這種訪問,所以應採用SyncRoot模式。即在類中定義一個私有的物件,將這個物件用於lock語句

public class Demo        
{            
	private object SyncRoot = new object();            
	public void DoThis()            
	{                
		lock(SyncRoot)                
		{                    
			// ...                
		}            
	}        
}

    使用鎖定是需要時間的,但是並總是需要的。因此可以建立類的兩個版本,一個同步版本,一個非同步版本。

public class Demo        
{            
	public virtual bool IsSynchorized            
	{                
		get { return false; }            
	}            
	/* 為了獲得類的同步版本,使用該方法傳遞一個非同步物件,返回一個SynchornizedDemo物件*/            
	public static Demo Synchornized(Demo d)            
	{
		if (!d.IsSynchorized)                    
			return new SynchornizedDemo(d);                
		return d;            
	}            
	public virtual void DoThis()            
	{            
	}           
	// 注意:使用SynchornizedDemo類時,只有方法是同步的,對這個類的兩個成員的呼叫並沒有同步            
	private class SynchornizedDemo : Demo            
	{                
		private object SyncRoot = new object();                
		private Demo d;                
		public SynchornizedDemo(Demo d)                
		{                    
			this.d = d;                
		}                
		public override bool IsSynchorized                
		{                   
			get { return true; }               
		}                
		public override void DoThis()                
		{                    
			lock(SyncRoot)                    
			{                        
				// ...                    
			}                
		}            
	}        
}

    警告:使用SyncRoot模式可能使執行緒安全產生負面影響。.Net1.0集合類實現了SyncRoot模式,.Net2.0的泛型集合類不再實現這個模式。

    比如:如果使用syncRoot模式鎖定物件的屬性訪問器,使該類變成執行緒安全的,但是仍會出現競態。因為在給物件屬性賦值時,在呼叫物件屬性的get和set訪問器期間,物件沒有鎖定,另一個執行緒可以獲取臨時值。

 

2、Interlocked類

      Interlocked類用於使變數的簡單語句原子化。它提供了以執行緒安全的方式遞增、遞減和交換值的方法。比如i++就不是執行緒安全的。

名稱說明
Add已過載。 以原子操作的形式,新增兩個整數並用兩者的和替換第一個整數。
CompareExchange已過載。 比較兩個值是否相等,如果相等,則替換其中一個值。
Decrement已過載。 以原子操作的形式遞減指定變數的值並儲存結果。
Exchange已過載。 以原子操作的形式將變數設定為指定的值。
Increment已過載。 以原子操作的形式遞增指定變數的值並儲存結果。
Read返回一個以原子操作形式載入的 64 位值。

      具體使用可參考MSDN

3、Monitor類(監視器同步)

     C#的lock語句就被編譯器解析為使用Monitor類。

     Monitor 物件通過使用 Monitor.EnterMonitor.TryEnterMonitor.Exit 方法對特定物件獲取鎖和釋放鎖來公開同步訪問程式碼區域的能力。在對程式碼區域獲取鎖後,就可以使用 Monitor.WaitMonitor.PulseMonitor.PulseAll 方法了。如果鎖被暫掛,則 Wait 釋放該鎖並等待通知。當 Wait 接到通知後,它將返回並再次獲取該鎖。PulsePulseAll 都會發出訊號以便等待佇列中的下一個執行緒繼續執行。

     與lock語句相比,monitor類的主要優點是:可以新增一個等待獲得鎖定的超時值。這樣就不會無限期地等待獲得鎖定,而使用TryEnter方法,給它傳送一個超時值,確定等待獲得鎖定的最長時間。如果得到了obj的鎖定,就可訪問由物件obj鎖定的狀態;否則,執行緒等待超時後不再等待,而是執行其他操作。也許以後,該執行緒會嘗試再次獲得該鎖定。

object obj = new object();            
// 1st使用方法            
System.Threading.Monitor.Enter(obj);            
try            
{                
	// ...            
}            
finally            
{
	System.Threading.Monitor.Exit(obj);            
}            
// 2nd使用方法            
if (System.Threading.Monitor.TryEnter(obj, 500))            
{                
	try                
	{                    
		// ...                
	}                
	finally                
	{
		System.Threading.Monitor.Exit(obj);                
	}            
}            
else            
{               
	// ...           
}

4、等待控制程式碼類WaitHandle

      在您的應用程式一次只處理一個非同步操作時,用於處理非同步操作的回撥和輪詢模型十分有用。 等待模型提供了一種更靈活的方式來處理多個非同步操作。 有兩種等待模型,是根據用於實現它們的 WaitHandle 方法命名的: 等待(任何)模型和等待(所有)模型。

      要使用上述任一等待模型,您需要使用 BeginExecuteNonQueryBeginExecuteReaderBeginExecuteXmlReader 方法返回的 IAsyncResult 物件的 AsyncWaitHandle 屬性。 WaitAnyWaitAll 方法都要求您將多個 WaitHandle 物件一起組合在一個陣列中,作為一個引數傳送。

      這兩種等待方法都監控非同步操作,等待操作完成。 WaitAny 方法等待任何操作完成或超時。 一旦您知道某一特定操作完成後,就可以處理其結果,然後繼續等待下一個操作完成或超時。 WaitAll 方法等待 WaitHandle 例項陣列中的所有程式都完成或超時後,再繼續。

名稱說明
SignalAndWait已過載。 以原子操作的形式,向一個 WaitHandle 發出訊號並等待另一個。
WaitAll已過載。 等待指定陣列中的所有元素都收到訊號。
WaitAny已過載。 等待指定陣列中的任一元素收到訊號。
WaitOne已過載。 阻止當前執行緒,直到當前 WaitHandle 收到訊號。

     類Mutex,Semaohone和Event派生自基類WaitHandle。

5、Mutex類(互斥)

      可以使用 Mutex 物件提供對資源的獨佔訪問。Mutex 類比 Monitor 類使用更多系統資源,但是它可以跨應用程式域邊界進行封送處理,可用於多個等待,並且可用於同步不同程式中的執行緒。

      執行緒呼叫 mutex 的 WaitOne 方法請求所有權。該呼叫會一直阻塞到 mutex 可用,或直至達到可選的超時間隔。如果沒有任何執行緒擁有它,則 Mutex 的狀態為已發訊號的狀態。

       執行緒通過呼叫其 ReleaseMutex 方法釋放 mutex。mutex 具有執行緒關聯;即 mutex 只能由擁有它的執行緒釋放。如果執行緒釋放不是它擁有的 mutex,則會在該執行緒中引發 ApplicationException

       由於 Mutex 類從 WaitHandle 派生,所以您還可以結合其他等待控制程式碼呼叫 WaitHandle 的靜態 WaitAllWaitAny 方法請求 Mutex 的所有權。

       如果某個執行緒擁有 Mutex,則該執行緒就可以在重複的等待-請求呼叫中指定同一個 Mutex,而不必阻止其執行;但是,它必須釋放 Mutex,次數與釋放所屬權的次數相同。

class Resource 
{ 
	Mutex m = new System.Threading.Mutex(); 
	public void Access(Int32 threadNum) 
	{ 
		m.WaitOne(); 
		try 
		{ 
			Console.WriteLine("Start Resource access (Thread={0})", threadNum); 
			System.Threading.Thread.Sleep(500); 
			Console.WriteLine("Stop  Resource access (Thread={0})", threadNum); 
		} 
		finally 
		{ 
			m.ReleaseMutex(); 
		} 
	} 
}

6、Semaohone類(訊號量)

      它是一種計數的互斥鎖定。與互斥鎖定的區別是,它可以同時由多個執行緒使用。

      使用Semaohone類鎖定,可以定義允許同時訪問受訊號量鎖定保護的資源的執行緒個數。如果有許多資源,且只有一定數量的執行緒訪問該資源,就可以使用該鎖定。

    程式設計人員應負責確保執行緒釋放訊號量的次數不會過多。例如,假定訊號量的最大計數為二,執行緒 A 和執行緒 B 都進入訊號量。如果執行緒 B 中發生了一個程式設計錯誤,導致它呼叫 Release 兩次,則兩次呼叫都會成功。這樣,訊號量的計數就已經達到了最大值,所以,當執行緒 A 最終呼叫 Release 時,將引發 SemaphoreFullException

public class Example    
{        
	// A semaphore that simulates a limited resource pool.        
	private static Semaphore _pool;        
	// A padding interval to make the output more orderly.        
	private static int _padding;        
	public static void Main()        
	{            
		// 訊號量計數為3,初始為0            
		_pool = new System.Threading.Semaphore(0, 3);            
		// 建立並啟動5個執行緒            
		for (int i = 1; i <= 5; i++)            
		{                
			System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(Worker));                
			t.Start(i);            
		}            
		// Wait for half a second, to allow all the            
		// threads to start and to block on the semaphore.            
		System.Threading.Thread.Sleep(500);            
		// The main thread starts out holding the entire            
		// semaphore count. Calling Release(3) brings the             
		// semaphore count back to its maximum value, and            
		// allows the waiting threads to enter the semaphore,            
		// up to three at a time.            
		Console.WriteLine("Main thread calls Release(3).");            
		_pool.Release(3);            
		Console.WriteLine("Main thread exits.");        
	}        
	private static void Worker(object num)       
	{            
		// Each worker thread begins by requesting the semaphore.           
		Console.WriteLine("Thread {0} begins and waits for the semaphore.", num);            
		// 鎖定            
		_pool.WaitOne();           
		// A padding interval to make the output more orderly.            
		int padding = System.Threading.Interlocked.Add(ref _padding, 100);            
		Console.WriteLine("Thread {0} enters the semaphore.", num);            
		// The thread's "work" consists of sleeping for             
		// about a second. Each thread "works" a little             
		// longer, just to make the output more orderly.           
		System.Threading.Thread.Sleep(1000 + padding);            
		Console.WriteLine("Thread {0} releases the semaphore.", num);            
		Console.WriteLine("Thread {0} previous semaphore count: {1}", num, _pool.Release());        
	}    
}

7、Events類

      事件是另一個系統級的資源同步方法。

      1)AutoResetEvent 類表示一個本地等待處理事件,在釋放了單個等待執行緒以後,該事件會在終止時自動重置。該類表示它的基類(即   EventWaitHandle)的特殊情況。有關自動重置事件的使用和功能,請參見 EventWaitHandle 概念文件。

      在釋放了單個等待執行緒以後,系統會自動將一個 AutoResetEvent 物件重置為非終止。如果沒有執行緒在等待,事件物件的狀態會保持為終止。AutoResetEvent 對應於 Win32 CreateEvent 呼叫,從而為 bManualReset 引數指定 false

      2)ManualResetEvent 類表示一個本地等待處理事件,在已發事件訊號後必須手動重置該事件。此類表示其基類 EventWaitHandle 的一種特殊情況。有關手動重置事件的用法和功能,請參見 EventWaitHandle 概念文件。

      在呼叫 ManualResetEvent 物件的 Reset 方法之前,該物件始終保持已發訊號狀態。在物件保持已發訊號狀態期間,可以釋放任意數目的等待執行緒或在已發事件訊號後仍等待事件的執行緒。ManualResetEvent 對應 Win32 CreateEvent 呼叫,它為 bManualReset 引數指定 true

      可以使用事件通知其他執行緒:這裡有一些資料,完成了一些操作等。事件可以發訊號也可以不發訊號。

class CalculateTest    
{        
	static void Main()        
	{            
		Calculate calc = new Calculate();            
		Console.WriteLine("Result = {0}.", calc.Result(234).ToString());            
		Console.WriteLine("Result = {0}.", calc.Result(55).ToString());        
	}    
}    
class Calculate    
{        
	double baseNumber, firstTerm, secondTerm, thirdTerm;        
	System.Threading.AutoResetEvent[] autoEvents;       
	System.Threading.ManualResetEvent manualEvent;        
	// Generate random numbers to simulate the actual calculations.       
	Random randomGenerator;        
	public Calculate()        
	{            
		autoEvents = new System.Threading.AutoResetEvent[]{     
			new System.Threading.AutoResetEvent(false),            
			new System.Threading.AutoResetEvent(false),            
			new System.Threading.AutoResetEvent(false)        
		};            
		manualEvent = new System.Threading.ManualResetEvent(false);        
	}        
	void CalculateBase(object stateInfo)        
	{            
		baseNumber = randomGenerator.NextDouble();            
		// Signal that baseNumber is ready.           
		manualEvent.Set();        
	}        
	// The following CalculateX methods all perform the same        
	// series of steps as commented in CalculateFirstTerm.        
	void CalculateFirstTerm(object stateInfo)        
	{            
		// Perform a precalculation.            
		double preCalc = randomGenerator.NextDouble();            
		// Wait for baseNumber to be calculated.           
		manualEvent.WaitOne();            
		// Calculate the first term from preCalc and baseNumber.           
		firstTerm = preCalc * baseNumber * randomGenerator.NextDouble();            
		// Signal that the calculation is finished.            
		autoEvents[0].Set();        
	}        
	void CalculateSecondTerm(object stateInfo)        
	{            
		double preCalc = randomGenerator.NextDouble();            
		manualEvent.WaitOne();            
		secondTerm = preCalc * baseNumber * randomGenerator.NextDouble();            
		autoEvents[1].Set();        
	}        
	void CalculateThirdTerm(object stateInfo)        
	{           
		double preCalc = randomGenerator.NextDouble();           
		manualEvent.WaitOne();            
		thirdTerm = preCalc * baseNumber * randomGenerator.NextDouble();            
		autoEvents[2].Set();        
	}        
	public double Result(int seed)        
	{            
		randomGenerator = new Random(seed);           
		// Simultaneously calculate the terms.            
		System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(CalculateBase));
		System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(CalculateFirstTerm));
		System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(CalculateSecondTerm));
		System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(CalculateThirdTerm));            
		// Wait for all of the terms to be calculated.            
		System.Threading.WaitHandle.WaitAll(autoEvents);            
		// Reset the wait handle for the next calculation.            
		manualEvent.Reset();            
		return firstTerm + secondTerm + thirdTerm;        
	}   
}

8、ReaderWriterLockSlim(讀取器/編寫器鎖)

       ReaderWriterLockSlim 類允許多個執行緒同時讀取一個資源,但在向該資源寫入時要求執行緒等待以獲得獨佔鎖。

       可以在應用程式中使用 ReaderWriterLockSlim,以便在訪問一個共享資源的執行緒之間提供協調同步。獲得的鎖是針對 ReaderWriterLockSlim 本身的。

       與任何執行緒同步機制相同,您必須確保任何執行緒都不會跳過 ReaderWriterLockSlim 提供的鎖定。確保做到這一點的一種方法是設計一個封裝該共享資源的類。此類將提供訪問專用共享資源以及使用專用 ReaderWriterLockSlim 進行同步的成員。ReaderWriterLockSlim 足夠有效,可用於同步各個物件。

       設計您應用程式的結構,讓讀取和寫入操作的時間儘可能最短。因為寫入鎖是排他的,所以長時間的寫入操作會直接影響吞吐量。長時間的讀取操作會阻止處於等待狀態的編寫器,並且,如果至少有一個執行緒在等待寫入訪問,則請求讀取訪問的執行緒也將被阻止。

       .NET Framework 有兩個讀取器-編寫器鎖,即 ReaderWriterLockSlimReaderWriterLock。建議在所有新的開發工作中使用 ReaderWriterLockSlimReaderWriterLockSlim 類似於 ReaderWriterLock,只是簡化了遞迴及升級和降級鎖定狀態的規則。ReaderWriterLockSlim 可避免多種潛在的死鎖情況。此外,ReaderWriterLockSlim 的效能明顯優於 ReaderWriterLock

 

名稱說明
EnterReadLock嘗試進入讀取模式鎖定狀態。
EnterUpgradeableReadLock嘗試進入可升級模式鎖定狀態。
EnterWriteLock嘗試進入寫入模式鎖定狀態。
ExitReadLock減少讀取模式的遞迴計數,並在生成的計數為 0(零)時退出讀取模式。
ExitUpgradeableReadLock減少可升級模式的遞迴計數,並在生成的計數為 0(零)時退出可升級模式。
ExitWriteLock減少寫入模式的遞迴計數,並在生成的計數為 0(零)時退出寫入模式。
TryEnterReadLock已過載。 嘗試進入讀取模式鎖定狀態,可以選擇超時時間。
TryEnterUpgradeableReadLock已過載。 嘗試進入可升級模式鎖定狀態,可以選擇超時時間。
TryEnterWriteLock已過載。 嘗試進入寫入模式鎖定狀態,可以選擇超時時間。

 

      ReaderWriterLockSlim 可以處於以下四種狀態之一:未進入、讀取、升級和寫入。

  • 未進入:在此狀態下,沒有任何執行緒進入鎖定狀態(或者所有執行緒都已退出鎖定狀態)。

  • 讀取:在此狀態下,一個或多個執行緒已進入受保護資源的讀訪問鎖定狀態。

    說明:

    執行緒既可通過使用 EnterReadLockTryEnterReadLock 方法進入讀取模式的鎖定,也可通過從可升級模式降級進入。

  • 升級:在此狀態下,一個已進入鎖定狀態進行讀取訪問的執行緒包含升級為寫入訪問(即可升級模式)的選項,同時零個或多個執行緒已進入讀取訪問鎖定狀態。每次只有一個執行緒可以進入包含升級選項的鎖定,其他嘗試進入可升級模式的執行緒都將受到阻塞。

  • 寫入:在此狀態下,有一個執行緒已進入對受保護資源進行寫入訪問的鎖定。該執行緒獨佔了鎖定狀態。出於任何原因嘗試進入鎖定的任意其他執行緒都將受到阻塞。

      詳見MSDN

相關文章