C#線程同步的幾種方法

GPS平臺、網站建設、軟件開發、系統運維,找森大網絡科技!
http://cnsendnet.taobao.com
來自森大科技官方博客
http://www.cnsendblog.com/index.php/?p=405php

咱們在編程的時候,有時會使用多線程來解決問題,好比你的程序須要在後臺處理一大堆數據,但還要使用戶界面處於可操做狀態;或者你的程序須要訪問一些外部資源如數據庫或網絡文件等。這些狀況你均可以建立一個子線程去處理,然而,多線程不可避免地會帶來一個問題,就是線程同步的問題。若是這個問題處理很差,咱們就會獲得一些非預期的結果。
  在網上也看過一些關於線程同步的文章,其實線程同步有好幾種方法,下面我就簡單的作一下概括。
  1、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.
  如: react

public class A
{
private volatile int _i;
public int I
{
get { return _i; }
set { _i = value; }
}
}

但volatile並不能實現真正的同步,由於它的操做級別只停留在變量級別,而不是原子級別。若是是在單處理器系統中,是沒有任何問題的,變量在主存中沒有機會被其餘人修改,由於只有一個處理器,這就叫做processor Self-Consistency。但在多處理器系統中,可能就會有問題。 每一個處理器都有本身的data cach,並且被更新的數據也不必定會當即寫回到主存。因此可能會形成不一樣步,但這種狀況很難發生,由於cach的讀寫速度至關快,flush的頻率也至關高,只有在壓力測試的時候纔有可能發生,並且概率很是很是小。
  2、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一個字符串
  3、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:
C#線程同步的幾種方法
4、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,輸出信息以下:
C#線程同步的幾種方法
另外,Monitor還提供了三個靜態方法Monitor.Pulse(Object o),Monitor.PulseAll(Object o)和Monitor.Wait(Object o ) ,用來實現一種喚醒機制的同步。關於這三個方法的用法,能夠參考MSDN,這裏就不詳述了。
  5、Mutex
  在使用上,Mutex與上述的Monitor比較接近,不過Mutex不具有Wait,Pulse,PulseAll的功能,所以,咱們不能使用Mutex實現相似的喚醒的功能。不過Mutex有一個比較大的特色,Mutex是跨進程的,所以咱們能夠在同一臺機器甚至遠程的機器上的多個進程上使用同一個互斥體。儘管Mutex也能夠實現進程內的線程同步,並且功能也更強大,但這種狀況下,仍是推薦使用Monitor,由於Mutex類是win32封裝的,因此它所須要的互操做轉換更耗資源。
  6、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的寫入獨佔權,執行代碼後,輸出以下:
C#線程同步的幾種方法
能夠看到,當WriterThread獲取到寫入獨佔權後,任何其它讀取的線程都必須等待,直到WriterThread釋放掉寫入獨佔權後,才能獲取到數據的訪問權,應該注意的是,上述打印信息很明顯顯示出,能夠多個線程同時獲取數據的讀取權,這從ReadThread1和ReadThread2的信息交互輸出能夠看出。
  7、SynchronizationAttribute
  當咱們肯定某個類的實例在同一時刻只能被一個線程訪問時,咱們能夠直接將類標識成Synchronization的,這樣,CLR會自動對這個類實施同步機制,實際上,這裏面涉及到同步域的概念,當類按以下設計時,咱們能夠確保類的實例沒法被多個線程同時訪問
  1). 在類的聲明中,添加System.Runtime.Remoting.Contexts.SynchronizationAttribute屬性。
2). 繼承至System.ContextBoundObject
須要注意的是,要實現上述機制,類必須繼承至System.ContextBoundObject,換句話說,類必須是上下文綁定的。
一個示範類代碼以下:ide

[System.Runtime.Remoting.Contexts.Synchronization]
public class SynchronizedClass : System.ContextBoundObject
{

}

8、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)是爲了在第一個線程還在方法中時,第二個線程可以有足夠的時間進來。對每一個方法分別起了兩個線程,咱們先來看一下結果:
C#線程同步的幾種方法
能夠看出,對於線程1和2,也就是調用沒有加屬性的方法的線程,當線程2進入方法後,尚未離開,線程1有進來了,這就是說,方法沒有同步。咱們再來看看線程3和4,當線程3進來後,方法被鎖,直到線程3釋放了鎖之後,線程4才進來。
  9、同步事件和等待句柄
  用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(); 
}

咱們先來看一下輸出:
C#線程同步的幾種方法
在主函數中,首先建立一個AutoResetEvent的實例,參數false表示初始狀態爲非終止,若是是true的話,初始狀態則爲終止。而後建立並啓動一個子線程,在子線程中,經過調用AutoResetEvent的WaitOne方法,使子線程等待指定事件的發生。而後主線程等待一秒後,調用AutoResetEvent的Set方法,使狀態由非終止變爲終止,從新激活子線程。

GPS平臺、網站建設、軟件開發、系統運維,找森大網絡科技!
http://cnsendnet.taobao.com
來自森大科技官方博客
http://www.cnsendblog.com/index.php/?p=405

相關文章
相關標籤/搜索