總結得非常全面,所以全文轉載,原文:http://www.cnblogs.com/michaelxu/archive/2008/09/20/1293716.html
我們在編程的時候,有時會使用多線程來解決問題,比如你的程式需要在幕後處理一大堆資料,但還要使使用者介面處於可操作狀態;或者你的程式需要訪問一些外部資源如資料庫或網路檔案等。這些情況你都可以建立一個子線程去處理,然而,多線程不可避免地會帶來一個問題,就是線程同步的問題。如果這個問題處理不好,我們就會得到一些非預期的結果。
在網上也看過一些關於線程同步的文章,其實線程同步有好幾種方法,下面我就簡單的做一下歸納。
一、volatile關鍵字
volatile是最簡單的一種同步方法,當然簡單是要付出代價的。它只能在變數一級做同步,volatile的含義就是告訴處理器, 不要將我放入工作記憶體, 請直接在主存操作我。(【轉自www.bitsCN.com 】)因此,當多線程同時訪問該變數時,都將直接操作主存,從本質上做到了變數共用。
能夠被標識為volatile的必須是以下幾種類型:(摘自MSDN)
Any reference type.
Any pointer type (in an unsafe context).
The types sbyte, byte, short, ushort, int, uint, char, float, bool.
An enum type with an enum base type of byte, sbyte, short, ushort, int, or uint. 如:
public class A{ private volatile int _i; public int I { get { return _i; } set { _i = value; } }} 但volatile並不能實現真正的同步,因為它的操作層級只停留在變數層級,而不是原子層級。如果是在單一處理器系統中,是沒有任何問題的,變數在主存中沒有機會被其他人修改,因為只有一個處理器,這就叫作processor Self-Consistency。但在多處理器系統中,可能就會有問題。 每個處理器都有自己的data cach,而且被更新的資料也不一定會立即寫回到主存。所以可能會造成不同步,但這種情況很難發生,因為cach的讀寫速度相當快,flush的頻率也相當高,只有在壓力測試的時候才有可能發生,而且幾率非常非常小。
二、lock關鍵字
lock是一種比較好用的簡單的線程同步方式,它是通過為給定對象擷取互斥鎖來實現同步的。它可以保證當一個線程在關鍵程式碼片段的時候,另一個線程不會進來,它只能等待,等到那個線程對象被釋放,也就是說線程出了臨界區。用法:
public void Function() { object lockThis = new object (); lock (lockThis) { // Access thread-sensitive resources. }} lock的參數必須是基於參考型別的對象,不要是基本類型像bool,int什麼的,這樣根本不能同步,原因是lock的參數要求是對象,如果傳入int,勢必要發生裝箱操作,這樣每次lock的都將是一個新的不同的對象。最好避免使用public類型或不受程式控制的對象執行個體,因為這樣很可能導致死結。特別是不要使用字串作為lock的參數,因為字串被CLR“暫留”,就是說整個應用程式中給定的字串都只有一個執行個體,因此更容易造成死結現象。建議使用不被“暫留”的私人或受保護的成員作為參數。其實某些類已經提供了專門用於被鎖的成員,比如Array類型提供SyncRoot,許多其它集合類型也都提供了SyncRoot。
所以,使用lock應該注意以下幾點:
1、如果一個類的執行個體是public的,最好不要lock(this)。因為使用你的類的人也許不知道你用了lock,如果他new了一個執行個體,並且對這個執行個體上鎖,就很容易造成死結。
2、如果MyType是public的,不要lock(typeof(MyType))
3、永遠也不要lock一個字串
三、System.Threading.Interlocked
對於整數資料類型的簡單操作,可以用 Interlocked 類的成員來實現線程同步,存在於System.Threading命名空間。Interlocked類有以下方法:Increment , Decrement , Exchange 和CompareExchange 。使用Increment 和Decrement 可以保證對一個整數的加減為一個原子操作。Exchange 方法自動交換指定變數的值。CompareExchange 方法組合了兩個操作:比較兩個值以及根據比較的結果將第三個值儲存在其中一個變數中。比較和交換操作也是按原子操作執行的。如:
int i = 0 ;System.Threading.Interlocked.Increment( ref i);Console.WriteLine(i);System.Threading.Interlocked.Decrement( ref i);Console.WriteLine(i);System.Threading.Interlocked.Exchange( ref i, 100 );Console.WriteLine(i);System.Threading.Interlocked.CompareExchange( ref i, 10 , 100 );
Output:
四、Monitor
Monitor類提供了與lock類似的功能,不過與lock不同的是,它能更好的控制同步塊,當調用了Monitor的Enter(Object o)方法時,會擷取o的獨佔權,直到調用Exit(Object o)方法時,才會釋放對o的獨佔權,可以多次調用Enter(Object o)方法,只需要調用同樣次數的Exit(Object o)方法即可,Monitor類同時提供了TryEnter(Object o,[int])的一個重載方法,該方法嘗試擷取o對象的獨佔權,當擷取獨佔權失敗時,將返回false。
但使用 lock 通常比直接使用 Monitor 更可取,一方面是因為 lock 更簡潔,另一方面是因為 lock 確保了即使受保護的代碼引發異常,也可以釋放基礎監視器。這是通過 finally 中調用Exit來實現的。事實上,lock 就是用 Monitor 類來實現的。下面兩段代碼是等效的:
lock (x){ DoSomething();}等效於
object obj = ( object )x;System.Threading.Monitor.Enter(obj);try{ DoSomething();}finally{ System.Threading.Monitor.Exit(obj);}
關於用法,請參考下面的代碼:
private static object m_monitorObject = new object ();[STAThread]static void Main( string [] args){Thread thread = new Thread( new ThreadStart(Do));thread.Name = " Thread1 " ;Thread thread2 = new Thread( new ThreadStart(Do));thread2.Name = " Thread2 " ;thread.Start();thread2.Start();thread.Join();thread2.Join();Console.Read();}static void Do(){if ( ! Monitor.TryEnter(m_monitorObject)){Console.WriteLine( " Can't visit Object " + Thread.CurrentThread.Name);return ;}try{Monitor.Enter(m_monitorObject);Console.WriteLine( " Enter Monitor " + Thread.CurrentThread.Name);Thread.Sleep( 5000 );}finally{Monitor.Exit(m_monitorObject);}} 當線程1擷取了m_monitorObject對象獨佔權時,線程2嘗試調用TryEnter(m_monitorObject),此時會由於無法擷取獨佔權而返回false,輸出資訊如下:
另外,Monitor還提供了三個靜態方法 Monitor.Pulse(Object o),Monitor.PulseAll(Object o)和Monitor.Wait(Object o ) ,用來實現一種喚醒機制的同步。關於這三個方法的用法,可以參考MSDN,這裡就不詳述了。
五、Mutex
在使用上,Mutex與上述的Monitor比較接近,不過Mutex不具備Wait,Pulse,PulseAll的功能,因此,我們不能使用Mutex實作類別似的喚醒的功能。不過Mutex有一個比較大的特點,Mutex是跨進程的,因此我們可以在同一台機器甚至遠端機器上的多個進程上使用同一個互斥體。儘管Mutex也可以實現進程內的線程同步,而且功能也更強大,但這種情況下,還是推薦使用Monitor,因為Mutex類是win32封裝的,所以它所需要的互操作轉換更耗資源。
六、ReaderWriterLock
在考慮資源訪問的時候,慣性上我們會對資源實施lock機制,但是在某些情況下,我們僅僅需要讀取資源的資料,而不是修改資源的資料,在這種情況下擷取資源的獨佔權無疑會影響運行效率,因此.Net提供了一種機制,使用 ReaderWriterLock進行資源訪問時,如果在某一時刻資源並沒有擷取寫的獨佔權,那麼可以獲得多個讀的訪問權,單個寫入的獨佔權,如果某一時刻已經擷取了寫入的獨佔權,那麼其它讀取的訪問權必須進行等待,參考以下代碼:
private static ReaderWriterLock m_readerWriterLock = new ReaderWriterLock();private static int m_int = 0;[STAThread]static void Main(string[] args){Thread readThread = new Thread(new ThreadStart(Read));readThread.Name = "ReadThread1";Thread readThread2 = new Thread(new ThreadStart(Read));readThread2.Name = "ReadThread2";Thread writeThread = new Thread(new ThreadStart(Writer));writeThread.Name = "WriterThread";readThread.Start();readThread2.Start();writeThread.Start();readThread.Join();readThread2.Join();writeThread.Join();Console.ReadLine(); }private static void Read(){while (true){Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireReaderLock");m_readerWriterLock.AcquireReaderLock(10000);Console.WriteLine(String.Format("ThreadName : {0} m_int : {1}", Thread.CurrentThread.Name, m_int));m_readerWriterLock.ReleaseReaderLock();}}private static void Writer(){while (true){Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireWriterLock");m_readerWriterLock.AcquireWriterLock(1000);Interlocked.Increment(ref m_int);Thread.Sleep(5000);m_readerWriterLock.ReleaseWriterLock();Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " ReleaseWriterLock");}}在程式中,我們啟動兩個線程擷取m_int的讀取存取權,使用一個線程擷取m_int的寫入獨佔權,執行代碼後,輸出如下:
可以看到,當WriterThread擷取到寫入獨佔權後,任何其它讀取的線程都必須等待,直到WriterThread釋放掉寫入獨佔權後,才能擷取到資料的訪問權,應該注意的是,上述列印資訊很明顯顯示出,可以多個線程同時擷取資料的讀取權,這從ReadThread1和ReadThread2的資訊互動輸出可以看出。
七、SynchronizationAttribute
當我們確定某個類的執行個體在同一時刻只能被一個線程訪問時,我們可以直接將類標識成Synchronization的,這樣,CLR會自動對這個類實施同步機制,實際上,這裡面涉及到同步域的概念,當類按如下設計時,我們可以確保類的執行個體無法被多個線程同時訪問
1). 在類的聲明中,添加System.Runtime.Remoting.Contexts.SynchronizationAttribute屬性。
2). 繼承至System.ContextBoundObject
需要注意的是,要實現上述機制,類必須繼承至System.ContextBoundObject,換句話說,類必須是上下文綁定的。
一個示範類代碼如下:
[System.Runtime.Remoting.Contexts.Synchronization]public class SynchronizedClass : System.ContextBoundObject{}八、MethodImplAttribute
如果臨界區是跨越整個方法的,也就是說,整個方法內部的代碼都需要上鎖的話,使用MethodImplAttribute屬性會更簡單一些。這樣就不用在方法內部加鎖了,只需要在方法上面加上 [MethodImpl(MethodImplOptions.Synchronized)] 就可以了,MehthodImpl和MethodImplOptions都在命名空間System.Runtime.CompilerServices 裡面。但要注意這個屬性會使整個方法加鎖,直到方法返回,才釋放鎖。因此,使用上不太靈活。如果要提前釋放鎖,則應該使用Monitor或lock。我們來看一個例子:
[MethodImpl(MethodImplOptions.Synchronized)]public void DoSomeWorkSync(){Console.WriteLine( " DoSomeWorkSync() -- Lock held by Thread " +Thread.CurrentThread.GetHashCode());Thread.Sleep( 1000 );Console.WriteLine( " DoSomeWorkSync() -- Lock released by Thread " +Thread.CurrentThread.GetHashCode());}public void DoSomeWorkNoSync(){Console.WriteLine( " DoSomeWorkNoSync() -- Entered Thread is " +Thread.CurrentThread.GetHashCode());Thread.Sleep( 1000 );Console.WriteLine( " DoSomeWorkNoSync() -- Leaving Thread is " +Thread.CurrentThread.GetHashCode());}[STAThread]static void Main( string [] args){MethodImplAttr testObj = new MethodImplAttr();Thread t1 = new Thread( new ThreadStart(testObj.DoSomeWorkNoSync));Thread t2 = new Thread( new ThreadStart(testObj.DoSomeWorkNoSync));t1.Start();t2.Start();Thread t3 = new Thread( new ThreadStart(testObj.DoSomeWorkSync));Thread t4 = new Thread( new ThreadStart(testObj.DoSomeWorkSync));t3.Start();t4.Start();Console.ReadLine(); }這裡,我們有兩個方法,我們可以對比一下,一個是加了屬性MethodImpl 的DoSomeWorkSync(),一個是沒加的DoSomeWorkNoSync()。在方法中Sleep(1000)是為了在第一個線程還在方法中時,第二個線程能夠有足夠的時間進來。對每個方法分別起了兩個線程,我們先來看一下結果:
可以看出,對於線程1和2,也就是調用沒有加屬性的方法的線程,當線程2進入方法後,還沒有離開,線程1有進來了,這就是說,方法沒有同步。我們再來看看線程3和4,當線程3進來後,方法被鎖,直到線程3釋放了鎖以後,線程4才進來。
九、同步事件和等待控制代碼
用lock和Monitor可以很好地起到線程同步的作用,但它們無法實現線程之間傳遞事件。如果要實現線程同步的同時,線程之間還要有互動,就要用到同步事件。同步事件是有兩個狀態(終止和非終止)的對象,它可以用來啟用和掛起線程。
同步事件有兩種:AutoResetEvent和 ManualResetEvent。它們之間唯一不同的地方就是在啟用線程之後,狀態是否自動由終止變為非終止。AutoResetEvent自動變為非終止,就是說一個 AutoResetEvent只能啟用一個線程。而ManualResetEvent要等到它的Reset方法被調用,狀態才變為非終止,在這之前,ManualResetEvent可以啟用任意多個線程。
可以調用WaitOne、WaitAny或WaitAll來使線程等待事件。它們之間的區別可以查看MSDN。當呼叫事件的 Set方法時,事件將變為終止狀態,等待的線程被喚醒。
來看一個例子,這個例子是MSDN上的。因為事件只用於一個線程的啟用,所以使用 AutoResetEvent 或 ManualResetEvent 類都可以。
static AutoResetEvent autoEvent;static void DoWork(){Console.WriteLine(" worker thread started, now waiting on event");autoEvent.WaitOne();Console.WriteLine(" worker thread reactivated, now exiting");}[STAThread]static void Main(string[] args){autoEvent = new AutoResetEvent(false);Console.WriteLine("main thread starting worker thread");Thread t = new Thread(new ThreadStart(DoWork));t.Start();Console.WriteLine("main thrad sleeping for 1 second");Thread.Sleep(1000);Console.WriteLine("main thread signaling worker thread");autoEvent.Set();Console.ReadLine(); }我們先來看一下輸出:
在主函數中,首先建立一個AutoResetEvent的執行個體,參數false 表示初始狀態為非終止,如果是true的話,初始狀態則為終止。然後建立並啟動一個子線程,在子線程中,通過調用AutoResetEvent的 WaitOne方法,使子線程等待指定事件的發生。然後主線程等待一秒後,調用AutoResetEvent的Set方法,使狀態由非終止變為終止,重新啟用子線程。
參考:
1/MSDN(http://msdn.microsoft.com/zh-cn/library/ms173179(VS.80).aspx )
2/http://www.cnblogs.com/VincentWP/archive/2008/06/25/1229104.html