溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

C#實(shí)現(xiàn)線程同步有多少種方法?

發(fā)布時(shí)間:2020-05-29 15:30:28 來(lái)源:億速云 閱讀:374 作者:鴿子 欄目:編程語(yǔ)言

線程同步有好幾種方法,下面我就簡(jiǎn)單的做一下歸納。

  一、volatile關(guān)鍵字
  volatile是最簡(jiǎn)單的一種同步方法,當(dāng)然簡(jiǎn)單是要付出代價(jià)的。它只能在變量一級(jí)做同步,volatile的含義就是告訴處理器, 不要將我放入工作內(nèi)存, 請(qǐng)直接在主存操作我。)因此,當(dāng)多線程同時(shí)訪問該變量時(shí),都將直接操作主存,從本質(zhì)上做到了變量共享。
  能夠被標(biāo)識(shí)為volatile的必須是以下幾種類型:
?  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并不能實(shí)現(xiàn)真正的同步,因?yàn)樗牟僮骷?jí)別只停留在變量級(jí)別,而不是原子級(jí)別。如果是在單處理器系統(tǒng)中,是沒有任何問題的,變量在主存中沒有機(jī)會(huì)被其他人修改,因?yàn)橹挥幸粋€(gè)處理器,這就叫作processor Self-Consistency。但在多處理器系統(tǒng)中,可能就會(huì)有問題。 每個(gè)處理器都有自己的data cach,而且被更新的數(shù)據(jù)也不一定會(huì)立即寫回到主存。所以可能會(huì)造成不同步,但這種情況很難發(fā)生,因?yàn)閏ach的讀寫速度相當(dāng)快,flush的頻率也相當(dāng)高,只有在壓力測(cè)試的時(shí)候才有可能發(fā)生,而且?guī)茁史浅7浅P ?
  二、lock關(guān)鍵字
  lock是一種比較好用的簡(jiǎn)單的線程同步方式,它是通過(guò)為給定對(duì)象獲取互斥鎖來(lái)實(shí)現(xiàn)同步的。它可以保證當(dāng)一個(gè)線程在關(guān)鍵代碼段的時(shí)候,另一個(gè)線程不會(huì)進(jìn)來(lái),它只能等待,等到那個(gè)線程對(duì)象被釋放,也就是說(shuō)線程出了臨界區(qū)。用法:

public void Function() 
{
object lockThis = new object (); 
lock (lockThis)
{
// Access thread-sensitive resources. 
}
} 

lock的參數(shù)必須是基于引用類型的對(duì)象,不要是基本類型像bool,int什么的,這樣根本不能同步,原因是lock的參數(shù)要求是對(duì)象,如果傳入int,勢(shì)必要發(fā)生裝箱操作,這樣每次lock的都將是一個(gè)新的不同的對(duì)象。最好避免使用public類型或不受程序控制的對(duì)象實(shí)例,因?yàn)檫@樣很可能導(dǎo)致死鎖。特別是不要使用字符串作為lock的參數(shù),因?yàn)樽址籆LR“暫留”,就是說(shuō)整個(gè)應(yīng)用程序中給定的字符串都只有一個(gè)實(shí)例,因此更容易造成死鎖現(xiàn)象。建議使用不被“暫留”的私有或受保護(hù)成員作為參數(shù)。其實(shí)某些類已經(jīng)提供了專門用于被鎖的成員,比如Array類型提供SyncRoot,許多其它集合類型也都提供了SyncRoot。
  所以,使用lock應(yīng)該注意以下幾點(diǎn): 
 ?。?、如果一個(gè)類的實(shí)例是public的,最好不要lock(this)。因?yàn)槭褂媚愕念惖娜艘苍S不知道你用了lock,如果他new了一個(gè)實(shí)例,并且對(duì)這個(gè)實(shí)例上鎖,就很容易造成死鎖。
 ?。病⑷绻鸐yType是public的,不要lock(typeof(MyType))
 ?。?、永遠(yuǎn)也不要lock一個(gè)字符串
  三、System.Threading.Interlocked
  對(duì)于整數(shù)數(shù)據(jù)類型的簡(jiǎn)單操作,可以用 Interlocked 類的成員來(lái)實(shí)現(xiàn)線程同步,存在于System.Threading命名空間。Interlocked類有以下方法:Increment , Decrement , Exchange 和CompareExchange 。使用Increment 和Decrement 可以保證對(duì)一個(gè)整數(shù)的加減為一個(gè)原子操作。Exchange 方法自動(dòng)交換指定變量的值。CompareExchange 方法組合了兩個(gè)操作:比較兩個(gè)值以及根據(jù)比較的結(jié)果將第三個(gè)值存儲(chǔ)在其中一個(gè)變量中。比較和交換操作也是按原子操作執(zhí)行的。如:

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#實(shí)現(xiàn)線程同步有多少種方法?
四、Monitor
  Monitor類提供了與lock類似的功能,不過(guò)與lock不同的是,它能更好的控制同步塊,當(dāng)調(diào)用了Monitor的Enter(Object o)方法時(shí),會(huì)獲取o的獨(dú)占權(quán),直到調(diào)用Exit(Object o)方法時(shí),才會(huì)釋放對(duì)o的獨(dú)占權(quán),可以多次調(diào)用Enter(Object o)方法,只需要調(diào)用同樣次數(shù)的Exit(Object o)方法即可,Monitor類同時(shí)提供了TryEnter(Object o,[int])的一個(gè)重載方法,該方法嘗試獲取o對(duì)象的獨(dú)占權(quán),當(dāng)獲取獨(dú)占權(quán)失敗時(shí),將返回false。
  但使用 lock 通常比直接使用 Monitor 更可取,一方面是因?yàn)?lock 更簡(jiǎn)潔,另一方面是因?yàn)?lock 確保了即使受保護(hù)的代碼引發(fā)異常,也可以釋放基礎(chǔ)監(jiān)視器。這是通過(guò) finally 中調(diào)用Exit來(lái)實(shí)現(xiàn)的。事實(shí)上,lock 就是用 Monitor 類來(lái)實(shí)現(xiàn)的。下面兩段代碼是等效的:

lock (x)
{
DoSomething();
} 
等效于

object obj = ( object )x;
System.Threading.Monitor.Enter(obj);
try 
{
DoSomething();
}
finally 
{
System.Threading.Monitor.Exit(obj);
} 

關(guān)于用法,請(qǐng)參考下面的代碼:

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);
}
} 

當(dāng)線程1獲取了m_monitorObject對(duì)象獨(dú)占權(quán)時(shí),線程2嘗試調(diào)用TryEnter(m_monitorObject),此時(shí)會(huì)由于無(wú)法獲取獨(dú)占權(quán)而返回false
另外,Monitor還提供了三個(gè)靜態(tài)方法Monitor.Pulse(Object o),Monitor.PulseAll(Object o)和Monitor.Wait(Object o ) ,用來(lái)實(shí)現(xiàn)一種喚醒機(jī)制的同步。關(guān)于這三個(gè)方法的用法,可以參考MSDN,這里就不詳述了。
  五、Mutex
  在使用上,Mutex與上述的Monitor比較接近,不過(guò)Mutex不具備Wait,Pulse,PulseAll的功能,因此,我們不能使用Mutex實(shí)現(xiàn)類似的喚醒的功能。不過(guò)Mutex有一個(gè)比較大的特點(diǎn),Mutex是跨進(jìn)程的,因此我們可以在同一臺(tái)機(jī)器甚至遠(yuǎn)程的機(jī)器上的多個(gè)進(jìn)程上使用同一個(gè)互斥體。盡管Mutex也可以實(shí)現(xiàn)進(jìn)程內(nèi)的線程同步,而且功能也更強(qiáng)大,但這種情況下,還是推薦使用Monitor,因?yàn)镸utex類是win32封裝的,所以它所需要的互操作轉(zhuǎn)換更耗資源。
  六、ReaderWriterLock
  在考慮資源訪問的時(shí)候,慣性上我們會(huì)對(duì)資源實(shí)施lock機(jī)制,但是在某些情況下,我們僅僅需要讀取資源的數(shù)據(jù),而不是修改資源的數(shù)據(jù),在這種情況下獲取資源的獨(dú)占權(quán)無(wú)疑會(huì)影響運(yùn)行效率,因此.Net提供了一種機(jī)制,使用ReaderWriterLock進(jìn)行資源訪問時(shí),如果在某一時(shí)刻資源并沒有獲取寫的獨(dú)占權(quán),那么可以獲得多個(gè)讀的訪問權(quán),單個(gè)寫入的獨(dú)占權(quán),如果某一時(shí)刻已經(jīng)獲取了寫入的獨(dú)占權(quán),那么其它讀取的訪問權(quán)必須進(jìn)行等待,參考以下代碼:

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");
}
} 

在程序中,我們啟動(dòng)兩個(gè)線程獲取m_int的讀取訪問權(quán),使用一個(gè)線程獲取m_int的寫入獨(dú)占權(quán),執(zhí)行代碼后,輸出如下:
C#實(shí)現(xiàn)線程同步有多少種方法?
可以看到,當(dāng)WriterThread獲取到寫入獨(dú)占權(quán)后,任何其它讀取的線程都必須等待,直到WriterThread釋放掉寫入獨(dú)占權(quán)后,才能獲取到數(shù)據(jù)的訪問權(quán),應(yīng)該注意的是,上述打印信息很明顯顯示出,可以多個(gè)線程同時(shí)獲取數(shù)據(jù)的讀取權(quán),這從ReadThread1和ReadThread2的信息交互輸出可以看出。
  七、SynchronizationAttribute
  當(dāng)我們確定某個(gè)類的實(shí)例在同一時(shí)刻只能被一個(gè)線程訪問時(shí),我們可以直接將類標(biāo)識(shí)成Synchronization的,這樣,CLR會(huì)自動(dòng)對(duì)這個(gè)類實(shí)施同步機(jī)制,實(shí)際上,這里面涉及到同步域的概念,當(dāng)類按如下設(shè)計(jì)時(shí),我們可以確保類的實(shí)例無(wú)法被多個(gè)線程同時(shí)訪問
  1). 在類的聲明中,添加System.Runtime.Remoting.Contexts.SynchronizationAttribute屬性。
2). 繼承至System.ContextBoundObject
需要注意的是,要實(shí)現(xiàn)上述機(jī)制,類必須繼承至System.ContextBoundObject,換句話說(shuō),類必須是上下文綁定的。
一個(gè)示范類代碼如下:

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

} 

八、MethodImplAttribute
  如果臨界區(qū)是跨越整個(gè)方法的,也就是說(shuō),整個(gè)方法內(nèi)部的代碼都需要上鎖的話,使用MethodImplAttribute屬性會(huì)更簡(jiǎn)單一些。這樣就不用在方法內(nèi)部加鎖了,只需要在方法上面加上 [MethodImpl(MethodImplOptions.Synchronized)] 就可以了,MehthodImpl和MethodImplOptions都在命名空間System.Runtime.CompilerServices 里面。但要注意這個(gè)屬性會(huì)使整個(gè)方法加鎖,直到方法返回,才釋放鎖。因此,使用上不太靈活。如果要提前釋放鎖,則應(yīng)該使用Monitor或lock。我們來(lái)看一個(gè)例子:

[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(); 
} 

這里,我們有兩個(gè)方法,我們可以對(duì)比一下,一個(gè)是加了屬性MethodImpl的DoSomeWorkSync(),一個(gè)是沒加的DoSomeWorkNoSync()。在方法中Sleep(1000)是為了在第一個(gè)線程還在方法中時(shí),第二個(gè)線程能夠有足夠的時(shí)間進(jìn)來(lái)。對(duì)每個(gè)方法分別起了兩個(gè)線程,我們先來(lái)看一下結(jié)果:
C#實(shí)現(xiàn)線程同步有多少種方法?
可以看出,對(duì)于線程1和2,也就是調(diào)用沒有加屬性的方法的線程,當(dāng)線程2進(jìn)入方法后,還沒有離開,線程1有進(jìn)來(lái)了,這就是說(shuō),方法沒有同步。我們?cè)賮?lái)看看線程3和4,當(dāng)線程3進(jìn)來(lái)后,方法被鎖,直到線程3釋放了鎖以后,線程4才進(jìn)來(lái)。
  九、同步事件和等待句柄
  用lock和Monitor可以很好地起到線程同步的作用,但它們無(wú)法實(shí)現(xiàn)線程之間傳遞事件。如果要實(shí)現(xiàn)線程同步的同時(shí),線程之間還要有交互,就要用到同步事件。同步事件是有兩個(gè)狀態(tài)(終止和非終止)的對(duì)象,它可以用來(lái)激活和掛起線程。
  同步事件有兩種:AutoResetEvent和 ManualResetEvent。它們之間唯一不同的地方就是在激活線程之后,狀態(tài)是否自動(dòng)由終止變?yōu)榉墙K止。AutoResetEvent自動(dòng)變?yōu)榉墙K止,就是說(shuō)一個(gè)AutoResetEvent只能激活一個(gè)線程。而ManualResetEvent要等到它的Reset方法被調(diào)用,狀態(tài)才變?yōu)榉墙K止,在這之前,ManualResetEvent可以激活任意多個(gè)線程。
  可以調(diào)用WaitOne、WaitAny或WaitAll來(lái)使線程等待事件。它們之間的區(qū)別可以查看MSDN。當(dāng)調(diào)用事件的 Set方法時(shí),事件將變?yōu)榻K止?fàn)顟B(tài),等待的線程被喚醒。
  來(lái)看一個(gè)例子,這個(gè)例子是MSDN上的。因?yàn)槭录挥糜谝粋€(gè)線程的激活,所以使用 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(); 
} 

我們先來(lái)看一下輸出:
C#實(shí)現(xiàn)線程同步有多少種方法?
在主函數(shù)中,首先創(chuàng)建一個(gè)AutoResetEvent的實(shí)例,參數(shù)false表示初始狀態(tài)為非終止,如果是true的話,初始狀態(tài)則為終止。然后創(chuàng)建并啟動(dòng)一個(gè)子線程,在子線程中,通過(guò)調(diào)用AutoResetEvent的WaitOne方法,使子線程等待指定事件的發(fā)生。然后主線程等待一秒后,調(diào)用AutoResetEvent的Set方法,使?fàn)顟B(tài)由非終止變?yōu)榻K止,重新激活子線程。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI