深入分析C# 線程同步
上一篇介紹了如何開啟線程,線程間相互傳遞參數(shù),及線程中本地變量和全局共享變量區(qū)別。
本篇主要說明線程同步。
如果有多個(gè)線程同時(shí)訪問共享數(shù)據(jù)的時(shí)候,就必須要用線程同步,防止共享數(shù)據(jù)被破壞。如果多個(gè)線程不會(huì)同時(shí)訪問共享數(shù)據(jù),可以不用線程同步。
線程同步也會(huì)有一些問題存在:
- 性能損耗。獲取,釋放鎖,線程上下文建切換都是耗性能的。
- 同步會(huì)使線程排隊(duì)等待執(zhí)行。
線程同步的幾種方法:
阻塞
當(dāng)線程調(diào)用Sleep,Join,EndInvoke,線程就處于阻塞狀態(tài)(Sleep使調(diào)用線程阻塞,Join、EndInvoke使另外一個(gè)線程阻塞),會(huì)立即從cpu退出。(阻塞狀態(tài)的線程不消耗cpu)
當(dāng)線程在阻塞和非阻塞狀態(tài)間切換時(shí)會(huì)消耗幾毫秒時(shí)間。
//Join static void Main() { Thread t = new Thread (Go); Console.WriteLine ("Main方法已經(jīng)運(yùn)行...."); t.Start(); t.Join();//阻塞Main方法 Console.WriteLine ("Main方法解除阻塞,繼續(xù)運(yùn)行..."); } static void Go() { Console.WriteLine ("在t線程上運(yùn)行Go方法..."); } //Sleep static void Main() { Console.WriteLine ("Main方法已經(jīng)運(yùn)行...."); Thread.CurrentThread.Sleep(3000);//阻塞當(dāng)前線程 Console.WriteLine ("Main方法解除阻塞,繼續(xù)運(yùn)行..."); } //Task static void Main() { Task Task1=Task.Run(() => { Console.WriteLine("task方法執(zhí)行..."); Thread.Sleep(1000); }); Console.WriteLine(Task1.IsCompleted); Task1.Wait();//阻塞主線程 ,等該Task1完成 Console.WriteLine(Task1.IsCompleted); }
加鎖(lock)
加鎖使多個(gè)線程同一時(shí)間只有一個(gè)線程可以調(diào)用該方法,其他線程被阻塞。
同步對象的選擇:
- 使用引用類型,值類型加鎖時(shí)會(huì)裝箱,產(chǎn)生一個(gè)新的對象。
- 使用private修飾,使用public時(shí)易產(chǎn)生死鎖。(使用lock(this),lock(typeof(實(shí)例))時(shí),該類也應(yīng)該是private)。
- string不能作為鎖對象。
- 不能在lock中使用
await
關(guān)鍵字
鎖是否必須是靜態(tài)類型?
如果被鎖定的方法是靜態(tài)的,那么這個(gè)鎖必須是靜態(tài)類型。這樣就是在全局鎖定了該方法,不管該類有多少個(gè)實(shí)例,都要排隊(duì)執(zhí)行。
如果被鎖定的方法不是靜態(tài)的,那么不能使用靜態(tài)類型的鎖,因?yàn)楸绘i定的方法是屬于實(shí)例的,只要該實(shí)例調(diào)用鎖定方法不產(chǎn)生損壞就可以,不同實(shí)例間是不需要鎖的。這個(gè)鎖只鎖該實(shí)例的方法,而不是鎖所有實(shí)例的方法.*
class ThreadSafe { private static object _locker = new object(); void Go() { lock (_locker) { ......//共享數(shù)據(jù)的操作 (Static Method),使用靜態(tài)鎖確保所有實(shí)例排隊(duì)執(zhí)行 } } private object _locker2=new object(); void GoTo() { lock(_locker2) //共享數(shù)據(jù)的操作,非靜態(tài)方法,是用非靜態(tài)鎖,確保同一個(gè)實(shí)例的方法調(diào)用者排隊(duì)執(zhí)行 } }
同步對象可以兼作它lock的對象
如:
class ThreadSafe { private List <string> _list = new List <string>(); void Test() { lock (_list) { _list.Add ("Item 1"); } } }
Monitors
lock
其實(shí)是Monitors
的簡潔寫法。
lock (x) { DoSomething(); }
兩者其實(shí)是一樣的。
System.Object obj = (System.Object)x; System.Threading.Monitor.Enter(obj); try { DoSomething(); } finally { System.Threading.Monitor.Exit(obj); }
互斥鎖(Mutex)
互斥鎖是一個(gè)互斥的同步對象,同一時(shí)間有且僅有一個(gè)線程可以獲取它??梢詫?shí)現(xiàn)進(jìn)程級(jí)別上線程的同步。
class Program { //實(shí)例化一個(gè)互斥鎖 public static Mutex mutex = new Mutex(); static void Main(string[] args) { for (int i = 0; i < 3; i++) { //在不同的線程中調(diào)用受互斥鎖保護(hù)的方法 Thread test = new Thread(MutexMethod); test.Start(); } Console.Read(); } public static void MutexMethod() { Console.WriteLine("{0} 請求獲取互斥鎖", Thread.CurrentThread.Name); mut.WaitOne(); Console.WriteLine("{0} 已獲取到互斥鎖", Thread.CurrentThread.Name); Thread.Sleep(1000); Console.WriteLine("{0} 準(zhǔn)備釋放互斥鎖", Thread.CurrentThread.Name); // 釋放互斥鎖 mut.ReleaseMutex(); Console.WriteLine("{0} 已經(jīng)釋放互斥鎖", Thread.CurrentThread.Name); } }
互斥鎖可以在不同的進(jìn)程間實(shí)現(xiàn)線程同步
使用互斥鎖實(shí)現(xiàn)一個(gè)一次只能啟動(dòng)一個(gè)應(yīng)用程序的功能。
public static class SingleInstance { private static Mutex m; public static bool IsSingleInstance() { //是否需要?jiǎng)?chuàng)建一個(gè)應(yīng)用 Boolean isCreateNew = false; try { m = new Mutex(initiallyOwned: true, name: "SingleInstanceMutex", createdNew: out isCreateNew); } catch (Exception ex) { } return isCreateNew; } }
互斥鎖的帶有三個(gè)參數(shù)的構(gòu)造函數(shù)
- initiallyOwned: 如果initiallyOwned為true,互斥鎖的初始狀態(tài)就是被所實(shí)例化的線程所獲取,否則實(shí)例化的線程處于未獲取狀態(tài)。
- name:該互斥鎖的名字,在操作系統(tǒng)中只有一個(gè)命名為name的互斥鎖mutex,如果一個(gè)線程得到這個(gè)name的互斥鎖,其他線程就無法得到這個(gè)互斥鎖了,必須等待那個(gè)線程對這個(gè)線程釋放。
- createNew:如果指定名稱的互斥體已經(jīng)存在就返回false,否則返回true。
信號(hào)和句柄
lock
和mutex
可以實(shí)現(xiàn)線程同步,確保一次只有一個(gè)線程執(zhí)行。但是線程間的通信就不能實(shí)現(xiàn)。如果線程需要相互通信的話就要使用AutoResetEvent
,ManualResetEvent
,通過信號(hào)來相互通信。它們都有兩個(gè)狀態(tài),終止?fàn)顟B(tài)和非終止?fàn)顟B(tài)。只有處于非終止?fàn)顟B(tài)時(shí),線程才可以阻塞。
AutoResetEvent:
AutoResetEvent
構(gòu)造函數(shù)可以傳入一個(gè)bool類型的參數(shù),false
表示將AutoResetEvent
對象的初始狀態(tài)設(shè)置為非終止。如果為true
標(biāo)識(shí)終止?fàn)顟B(tài),那么WaitOne
方法就不會(huì)再阻塞線程了。但是因?yàn)樵擃悤?huì)自動(dòng)的將終止?fàn)顟B(tài)修改為非終止,所以,之后再調(diào)用WaitOne
方法就會(huì)被阻塞。
WaitOne
方法如果AutoResetEvent
對象狀態(tài)非終止,則阻塞調(diào)用該方法的線程??梢灾付〞r(shí)間,若沒有獲取到信號(hào),返回false
set
方法釋放被阻塞的線程。但是一次只可以釋放一個(gè)被阻塞的線程。
class ThreadSafe { static AutoResetEvent autoEvent; static void Main() { //使AutoResetEvent處于非終止?fàn)顟B(tài) autoEvent = new AutoResetEvent(false); Console.WriteLine("主線程運(yùn)行..."); Thread t = new Thread(DoWork); t.Start(); Console.WriteLine("主線程sleep 1秒..."); Thread.Sleep(1000); Console.WriteLine("主線程釋放信號(hào)..."); autoEvent.Set(); } static void DoWork() { Console.WriteLine(" t線程運(yùn)行DoWork方法,阻塞自己等待main線程信號(hào)..."); autoEvent.WaitOne(); Console.WriteLine(" t線程DoWork方法獲取到main線程信號(hào),繼續(xù)執(zhí)行..."); } }
輸出
主線程運(yùn)行...
主線程sleep 1秒...
t線程運(yùn)行DoWork方法,阻塞自己等待main線程信號(hào)...
主線程釋放信號(hào)...
t線程DoWork方法獲取到main線程信號(hào),繼續(xù)執(zhí)行...
ManualResetEvent
ManualResetEvent
和AutoResetEvent
用法類似。
AutoResetEvent
在調(diào)用了Set
方法后,會(huì)自動(dòng)的將信號(hào)由釋放(終止)改為阻塞(非終止),一次只有一個(gè)線程會(huì)得到釋放信號(hào)。而ManualResetEvent
在調(diào)用Set
方法后不會(huì)自動(dòng)的將信號(hào)由釋放(終止)改為阻塞(非終止),而是一直保持釋放信號(hào),使得一次有多個(gè)被阻塞線程運(yùn)行,只能手動(dòng)的調(diào)用Reset
方法,將信號(hào)由釋放(終止)改為阻塞(非終止),之后的再調(diào)用Wait.One方法的線程才會(huì)被再次阻塞。
public class ThreadSafe { //創(chuàng)建一個(gè)處于非終止?fàn)顟B(tài)的ManualResetEvent private static ManualResetEvent mre = new ManualResetEvent(false); static void Main() { for(int i = 0; i <= 2; i++) { Thread t = new Thread(ThreadProc); t.Name = "Thread_" + i; t.Start(); } Thread.Sleep(500); Console.WriteLine("\n新線程的方法已經(jīng)啟動(dòng),且被阻塞,調(diào)用Set釋放阻塞線程"); mre.Set(); Thread.Sleep(500); Console.WriteLine("\n當(dāng)ManualResetEvent處于終止?fàn)顟B(tài)時(shí),調(diào)用由Wait.One方法的多線程,不會(huì)被阻塞。"); for(int i = 3; i <= 4; i++) { Thread t = new Thread(ThreadProc); t.Name = "Thread_" + i; t.Start(); } Thread.Sleep(500); Console.WriteLine("\n調(diào)用Reset方法,ManualResetEvent處于非阻塞狀態(tài),此時(shí)調(diào)用Wait.One方法的線程再次被阻塞"); mre.Reset(); Thread t5 = new Thread(ThreadProc); t5.Name = "Thread_5"; t5.Start(); Thread.Sleep(500); Console.WriteLine("\n調(diào)用Set方法,釋放阻塞線程"); mre.Set(); } private static void ThreadProc() { string name = Thread.CurrentThread.Name; Console.WriteLine(name + " 運(yùn)行并調(diào)用WaitOne()"); mre.WaitOne(); Console.WriteLine(name + " 結(jié)束"); } } //Thread_2 運(yùn)行并調(diào)用WaitOne() //Thread_1 運(yùn)行并調(diào)用WaitOne() //Thread_0 運(yùn)行并調(diào)用WaitOne() //新線程的方法已經(jīng)啟動(dòng),且被阻塞,調(diào)用Set釋放阻塞線程 //Thread_2 結(jié)束 //Thread_1 結(jié)束 //Thread_0 結(jié)束 //當(dāng)ManualResetEvent處于終止?fàn)顟B(tài)時(shí),調(diào)用由Wait.One方法的多線程,不會(huì)被阻塞。 //Thread_3 運(yùn)行并調(diào)用WaitOne() //Thread_4 運(yùn)行并調(diào)用WaitOne() //Thread_4 結(jié)束 //Thread_3 結(jié)束 ///調(diào)用Reset方法,ManualResetEvent處于非阻塞狀態(tài),此時(shí)調(diào)用Wait.One方法的線程再次被阻塞 //Thread_5 運(yùn)行并調(diào)用WaitOne() //調(diào)用Set方法,釋放阻塞線程 //Thread_5 結(jié)束
Interlocked
如果一個(gè)變量被多個(gè)線程修改,讀取。可以用Interlocked
。
計(jì)算機(jī)上不能保證對一個(gè)數(shù)據(jù)的增刪是原子性的,因?yàn)閷?shù)據(jù)的操作也是分步驟的:
- 將實(shí)例變量中的值加載到寄存器中。
- 增加或減少該值。
- 在實(shí)例變量中存儲(chǔ)該值。
Interlocked
為多線程共享的變量提供原子操作。
Interlocked
提供了需要原子操作的方法:
- public static int Add (ref int location1, int value); 兩個(gè)參數(shù)相加,且把結(jié)果和賦值該第一個(gè)參數(shù)。
- public static int Increment (ref int location); 自增。
- public static int CompareExchange (ref int location1, int value, int comparand);
location1 和comparand比較,被value替換.
value 如果第一個(gè)參數(shù)和第三個(gè)參數(shù)相等,那么就把value賦值給第一個(gè)參數(shù)。
comparand 和第一個(gè)參數(shù)對比。
ReaderWriterLock
如果要確保一個(gè)資源或數(shù)據(jù)在被訪問之前是最新的。那么就可以使用ReaderWriterLock
.該鎖確保在對資源獲取賦值或更新時(shí),只有它自己可以訪問這些資源,其他線程都不可以訪問。即排它鎖。但用改鎖讀取這些數(shù)據(jù)時(shí),不能實(shí)現(xiàn)排它鎖。
lock
允許同一時(shí)間只有一個(gè)線程執(zhí)行。而ReaderWriterLock
允許同一時(shí)間有多個(gè)線程可以執(zhí)行讀操作,或者只有一個(gè)有排它鎖的線程執(zhí)行寫操作。
class Program { // 創(chuàng)建一個(gè)對象 public static ReaderWriterLock readerwritelock = new ReaderWriterLock(); static void Main(string[] args) { //創(chuàng)建一個(gè)線程讀取數(shù)據(jù) Thread t1 = new Thread(Write); // t1.Start(1); Thread t2 = new Thread(Write); //t2.Start(2); // 創(chuàng)建10個(gè)線程讀取數(shù)據(jù) for (int i = 3; i < 6; i++) { Thread t = new Thread(Read); // t.Start(i); } Console.Read(); } // 寫入方法 public static void Write(object i) { // 獲取寫入鎖,20毫秒超時(shí)。 Console.WriteLine("線程:" + i + "準(zhǔn)備寫..."); readerwritelock.AcquireWriterLock(Timeout.Infinite); Console.WriteLine("線程:" + i + " 寫操作" + DateTime.Now); // 釋放寫入鎖 Console.WriteLine("線程:" + i + "寫結(jié)束..."); Thread.Sleep(1000); readerwritelock.ReleaseWriterLock(); } // 讀取方法 public static void Read(object i) { Console.WriteLine("線程:" + i + "準(zhǔn)備讀..."); // 獲取讀取鎖,20毫秒超時(shí) readerwritelock.AcquireReaderLock(Timeout.Infinite); Console.WriteLine("線程:" + i + " 讀操作" + DateTime.Now); // 釋放讀取鎖 Console.WriteLine("線程:" + i + "讀結(jié)束..."); Thread.Sleep(1000); readerwritelock.ReleaseReaderLock(); } } //分別屏蔽writer和reader方法??梢愿逦目吹?writer被阻塞了。而reader沒有被阻塞。 //屏蔽reader方法 //線程:1準(zhǔn)備寫... //線程:1 寫操作2017/7/5 17:50:01 //線程:1寫結(jié)束... //線程:2準(zhǔn)備寫... //線程:2 寫操作2017/7/5 17:50:02 //線程:2寫結(jié)束... //屏蔽writer方法 //線程:3準(zhǔn)備讀... //線程:5準(zhǔn)備讀... //線程:4準(zhǔn)備讀... //線程:5 讀操作2017/7/5 17:50:54 //線程:5讀結(jié)束... //線程:3 讀操作2017/7/5 17:50:54 //線程:3讀結(jié)束... //線程:4 讀操作2017/7/5 17:50:54 //線程:4讀結(jié)束...
參考:
- MSDN
- 《CLR via C#》
以上就是深入分析C# 線程同步的詳細(xì)內(nèi)容,更多關(guān)于c# 線程同步的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#中的lock、Monitor、Mutex學(xué)習(xí)筆記
這篇文章主要介紹了C#中的lock、Monitor、Mutex學(xué)習(xí)筆記,本文講解的都是線程同步的一些知識(shí),需要的朋友可以參考下2015-01-01C#中DataTable 轉(zhuǎn)換為 Json的方法匯總(三種方法)
JavaScript Object Notation (Json)是一種輕量級(jí)的數(shù)據(jù)交換格式,下面小編給大家介紹三種方法實(shí)現(xiàn)DataTable轉(zhuǎn)換成 Json 對象,感興趣的朋友一起看看吧2016-11-11C# 循環(huán)判斷會(huì)進(jìn)來幾次的實(shí)現(xiàn)代碼
這篇文章主要介紹了C# 循環(huán)判斷會(huì)進(jìn)來幾次的實(shí)現(xiàn)代碼,代碼中就一個(gè)循環(huán),循環(huán)的判斷是從一個(gè)函數(shù)獲取值,需要的朋友可以參考下2018-06-06