最近由于在準備Collection對象培訓的PPT,因為涉及到SyncRoot的屬性的講解,所以對怎樣在多線程應用程序中同步資源訪問做了個總結: 對于引用類型和非線程安全的資源的同步處理,有四種相關處理:lock關鍵字,監視器(Monitor), 同步事件和等待句柄, mutex類。 Lock關鍵字 本人愚鈍,在以前編程中遇到lock的問題總是使用lock(this)一鎖了之,出問題后翻看MSDN突然發現下面幾行字:通常,應避免鎖定 public 類型,否則實例將超出代碼的控制范圍。常見的結構 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 違反此準則:如果實例可以被公共訪問,將出現 lock (this) 問題。如果 MyType 可以被公共訪問,將出現 lock (typeof (MyType)) 問題。由于進程中使用同一字符串的任何其他代碼將共享同一個鎖,所以出現 lock(“myLock”) 問題。來看看lock(this)的問題:如果有一個類Class1,該類有一個方法用lock(this)來實現互斥: public void Method2() { lock (this) { System.Windows.Forms.MessageBox.Show("Method2 End"); } } 如果在同一個Class1的實例中,該Method2能夠互斥的執行。但是如果是2個Class1的實例分別來執行Method2,是沒有互斥效果的。因為這里的lock,只是對當前的實例對象進行了加鎖。 Lock(typeof(MyType))鎖定住的對象范圍更為廣泛,由于一個類的所有實例都只有一個類型對象(該對象是typeof的返回結果),鎖定它,就鎖定了該對象的所有實例,微軟現在建議(原文請參考:http://www.microsoft.com/china/MSDN/library/enterPRisedevelopment/softwaredev/SDaskgui06032003.mspx?mfr=true)不要使用lock(typeof(MyType)),因為鎖定類型對象是個很緩慢的過程,并且類中的其他線程、甚至在同一個應用程序域中運行的其他程序都可以訪問該類型對象,因此,它們就有可能代替您鎖定類型對象,完全阻止您的執行,從而導致你自己的代碼的掛起。 鎖住一個字符串更為神奇,只要字符串內容相同,就能引起程序掛起。原因是在.NET中,字符串會被暫時存放,如果兩個變量的字符串內容相同的話,.NET會把暫存的字符串對象分配給該變量。所以如果有兩個地方都在使用lock(“my lock”)的話,它們實際鎖住的是同一個對象。到此,微軟給出了個lock的建議用法:鎖定一個私有的static 成員變量。 .NET在一些集合類中(比如ArrayList,HashTable,Queue,Stack)已經提供了一個供lock使用的對象SyncRoot,用Reflector工具查看了SyncRoot屬性的代碼,在Array中,該屬性只有一句話:return this,這樣和lock array的當前實例是一樣的。ArrayList中的SyncRoot有所不同 get { if (this._syncRoot == null) { Interlocked.CompareExchange(ref this._syncRoot, new object(), null); } return this._syncRoot; 其中Interlocked類是專門為多個線程共享的變量提供原子操作(如果你想鎖定的對象是基本數據類型,那么請使用這個類),CompareExchange方法將當前syncRoot和null做比較,如果相等,就替換成new object(),這樣做是為了保證多個線程在使用syncRoot時是線程安全的。集合類中還有一個方法是和同步相關的:Synchronized,該方法返回一個對應的集合類的wrapper類,該類是線程安全的,因為他的大部分方法都用lock來進行了同步處理,比如Add方法: public override void Add(object key, object value) { lock (this._table.SyncRoot) { this._table.Add(key, value); } } 這里要特別注意的是MSDN提到:從頭到尾對一個集合進行枚舉本質上并不是一個線程安全的過程。即使一個集合已進行同步,其他線程仍可以修改該集合,這將導致枚舉數引發異常。若要在枚舉過程中保證線程安全,可以在整個枚舉過程中鎖定集合: Queue myCollection = new Queue(); lock(myCollection.SyncRoot) { foreach (Object item in myCollection) { // Insert your code here. } } Monitor類 該類功效和lock類似: System.Object obj = (System.Object)x; System.Threading.Monitor.Enter(obj); try { DoSomething(); } finally { System.Threading.Monitor.Exit(obj); } lock關鍵字比Monitor簡潔,其實lock就是對Monitor的Enter和Exit的一個封裝。另外Monitor還有幾個常用的方法:TryEnter能夠有效的決絕長期死等的問題,如果在一個并發經常發生,而且持續時間長的環境中使用TryEnter,可以有效防止死鎖或者長時間的等待。比如我們可以設置一個等待時間bool gotLock = Monitor.TryEnter(myobject,1000),讓當前線程在等待1000秒后根據返回的bool值來決定是否繼續下面的操作。Pulse以及PulseAll還有Wait方法是成對使用的,它們能讓你更精確的控制線程之間的并發,MSDN關于這3個方法的解釋很含糊,有必要用一個具體的例子來說明一下: using System.Threading; public class Program { static object ball = new object(); public static void Main() { Thread threadPing = new Thread( ThreadPingProc ); Thread threadPong = new Thread( ThreadPongProc ); threadPing.Start(); threadPong.Start(); }
static void ThreadPongProc() { System.Console.WriteLine("ThreadPong: Hello!"); lock ( ball ) for (int i = 0; i < 5; i++){ System.Console.WriteLine("ThreadPong: Pong "); Monitor.Pulse( ball ); Monitor.Wait( ball ); } System.Console.WriteLine("ThreadPong: Bye!"); } static void ThreadPingProc() { System.Console.WriteLine("ThreadPing: Hello!"); lock ( ball ) for(int i=0; i< 5; i++){ System.Console.WriteLine("ThreadPing: Ping "); Monitor.Pulse( ball ); Monitor.Wait( ball ); } System.Console.WriteLine("ThreadPing: Bye!"); } } 執行結果如下(有可能是ThreadPong先執行): ThreadPing: Hello! ThreadPing: Ping ThreadPong: Hello! ThreadPong: Pong ThreadPing: Ping ThreadPong: Pong ThreadPing: Ping ThreadPong: Pong ThreadPing: Ping ThreadPong: Pong ThreadPing: Ping ThreadPong: Pong ThreadPing: Bye! 當threadPing進程進入ThreadPingProc鎖定ball并調用Monitor.Pulse( ball );后,它通知threadPong從阻塞隊列進入準備隊列,當threadPing調用Monitor.Wait( ball )阻塞自己后,它放棄了了對ball的鎖定,所以threadPong得以執行。PulseAll與Pulse方法類似,不過它是向所有在阻塞隊列中的進程發送通知信號,如果只有一個線程被阻塞,那么請使用Pulse方法。 同步事件和等待句柄 同步事件和等待句柄用于解決更復雜的同步情況,比如一個一個大的計算步驟包含3個步驟result = first term + second term + third term,如果現在想寫個多線程程序,同時計算first term,second term 和third term,等所有3個步驟計算好后再把它們匯總起來,我們就需要使用到同步事件和等待句柄,同步事件分有兩個,分別為AutoResetEvent和ManualResetEvent,這兩個類可以用來代表某個線程的運行狀態:終止和非終止,等待句柄用來判斷ResetEvent的狀態,如果是非終止狀態就一直等待,否則放行,讓等待句柄下面的代碼繼續運行。下面的代碼示例闡釋了如何使用等待句柄來發送復雜數字計算的不同階段的完成信號。此計算的格式為:result = first term + second term + third term using System; using System.Threading; 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; AutoResetEvent[] autoEvents; ManualResetEvent manualEvent; // Generate random numbers to simulate the actual calculations. Random randomGenerator; public Calculate() { autoEvents = new AutoResetEvent[] { new AutoResetEvent(false), new AutoResetEvent(false), new AutoResetEvent(false) }; manualEvent = new 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)
// Simultaneously calculate the terms. ThreadPool.QueueUserWorkItem( new WaitCallback(CalculateBase)); ThreadPool.QueueUserWorkItem( new WaitCallback(CalculateFirstTerm)); ThreadPool.QueueUserWorkItem( new WaitCallback(CalculateSecondTerm)); ThreadPool.QueueUserWorkItem( new WaitCallback(CalculateThirdTerm)); // Wait for all of the terms to be calculated. WaitHandle.WaitAll(autoEvents); // Reset the wait handle for the next calculation. manualEvent.Reset(); return firstTerm + secondTerm + thirdTerm; } } 該示例一共有4個ResetEvent,一個ManualEvent,三個AutoResetEvent,分別反映4個線程的運行狀態。ManualEvent和AutoResetEvent有一點不同:AutoResetEvent是在當前線程調用set方法激活某線程之后,AutoResetEvent狀態自動重置,而ManualEvent則需要手動調用Reset方法來重置狀態。接著來看看上面那段代碼的執行順序,Main方法首先調用的是Result 方法,Result方法開啟4個線程分別去執行,主線程阻塞在WaitHandle.WaitAll(autoEvents)處,等待3個計算步驟的完成。4個ResetEvent初始化狀態都是非終止(構造實例時傳入了false),CalculateBase首先執行完畢,其他3個線程阻塞在manualEvent.WaitOne()處,等待CalculateBase執行完成。CalculateBase生成baseNumber后,把代表自己的ManualEvent狀態設置為終止狀態。其他幾個線程從manualEvent.WaitOne()處恢復執行,在執行完自己的代碼后把自己對應的AutoResetEvent狀態置為終止。當3個計算步驟執行完后,主線程從阻塞中恢復,把三個計算結果累加后返回。還要多補充一點的是WaitHandle的WaitOne,WaitAll,WaitAny方法,如果等待多個進程就用WaitAll,如本例中的:WaitHandle.WaitAll(autoEvents),WaitAny是等待的線程中有一個結束則停止等待。 Mutex 對象 Mutex與Monitor類似,這里不再累贅,需要注意的是Mutex分兩種:一種是本地Mutex一種是系統級Mutex,系統級Mutex可以用來進行跨進程間的線程的同步。盡管 mutex 可以用于進程內的線程同步,但是使用 Monitor 通常更為可取,因為監視器是專門為 .NET Framework 而設計的,因而它可以更好地利用資源。相比之下,Mutex 類是 Win32 構造的包裝。盡管 mutex 比監視器更為強大,但是相對于 Monitor 類,它所需要的互操作轉換更消耗計算資源。 注:文中代碼示例來源于MSDN和CodeProject