C# 溫故而知新: 線程篇(四)
線程同步篇 (中):同步工具類的介紹
- 1 上篇回顧
- 2 繼續介紹基元內核模式中的 monitor類
- 3 同步句柄:WaitHandle
- 4 EventWaitHandle,AutoResetEvent和ManualResetEvent
- 5 同步互斥mutex類
- 6 簡單說明下mutex和monitor的區別
- 7 選擇我們需要的同步工具
- 8 本章總結
很抱歉好久沒寫博客了,由於工作太忙,所以最近一段時間落下了,讓我們開始上一篇向大家介紹了下線程同步中的一些重要概念包括:
基元內核模式,基元用戶模式,原子性,然后由陸續介紹了基元用戶模式中的Validated,Interloced 和ReaderWriterLock 類,同時也簡單
介紹了下基元內核模式中的lock關鍵字,本章再讓我們繼續深入了解其他的一些同步工具類
首先上圖讓大家有個初步的印象:
Monitor類也是同步機制中比較重要的一個類,它屬於基元內核模式中的一種,也是上一章中與lock關鍵字有着密切關系,Monitor類采取
排他鎖來進行對共享區的同步,當一個線程進入共享區時,會取得排他鎖的控制權,其他線程則必須等待,大伙注意,這里有2個重要的線
程狀態需要在說明下
1:等待隊列: 等待進入共享區的線程會首先進入到等待隊列中,等待持有排他鎖的線程通知某個等待線程進入到就緒隊列中,注意(只有擁 有排他鎖的線程才能進行互換通知功能,甚至該線程能夠喚醒一堆的等待線程進入到就緒隊列中) 2:就緒隊列 等待隊列中的某個線程被持有排他鎖的線程喚醒放入到就緒隊列中,等待獲取排他鎖的機會,這樣一個周期便可以連接起來, 線程從等待到被喚醒到就緒狀態,然后獲取排他鎖進如共享區操作,然后交出排他鎖等待或者睡眠,直到再次被喚醒。 |
在這里強調下Monitor是個十分容易產生死鎖的同步類,其原因是:
1.當一個線程試圖去請求鎖對象時,它不是處在等待隊列,而是就緒隊列,如果需要讓其進入等待隊列,則必須使用Wait方法
2.當一個線程釋放鎖對象時是不會通知等待隊列中的線程進入到就緒隊列,需要通過Palse方法
3.線程啟動的時候,線程是處於就緒狀態的
4.就算處於就緒狀態的線程被cpu選中,但是一旦數據被鎖定,那個線程還是無法獲取到控制權
其實大家了解原因后對於monitor已經算了解,這正是monitor的機制
了解了上述機制后,大家可以開始理解該類比較重要的幾個方法:
Monitor. Enter(Object);
該方法旨在宣布當前線程進入了臨界區,持有了排他鎖,其他線程繼續等待,直到該線程離開共享區交出排他鎖給就緒隊列中的一個線程
Monitor. Exit(Object);
當持有共享鎖的線程執行完任務之后,該線程變通過這個方法離開共享區,臨走前可以操作其喚醒一個或一堆的等待線程
Monitor.Palse(Object)和Monitor.Palse(Object)
這兩個方法比較復雜和相似,也就是喚醒(改變)其他線程狀態的方法,持有排他鎖的線程利用這兩個方法通知其他線程進入到就緒隊列,離開等待隊列
Monitor.Wait(Object)
這個方法也是非常的重要,假如在共享區內的線程執行了wait方法后,該線程會被放入等待隊列中,從而失去了排他鎖的控制權,就緒隊列中的下一個線程就進入到了臨界區
Monitor.TryEnter(Object, Boolean))
有時其他線程希望通過進行嘗試的方式主動去爭取排他鎖的控制權,這個方法便能實現這個功能,同時通過一個BOOL參數來指示會否占有了排他鎖
文字介紹為了讓大伙更好的理解Monitor類的本質,接下來就上很簡單代碼讓我們更深入的了解
/// <summary> /// 開啟2個寫線程,2個讀線程演示 /// </summary> /// <param name="args"></param> static void Main(string[] args) { Thread t1 = new Thread(new ThreadStart(WriteInShareArea)); Thread t3 = new Thread(new ThreadStart(WriteInShareArea)); Thread t2 = new Thread(new ThreadStart(Read)); Thread t4= new Thread(new ThreadStart(Read)); Console.WriteLine("t1's id={0}", t1.ManagedThreadId); Console.WriteLine("t2's id={0}", t2.ManagedThreadId); Console.WriteLine("t3's id={0}", t3.ManagedThreadId); t1.Start(); t3.Start(); t2.Start(); t4.Start(); Console.Read(); } /// <summary> /// 讀數據,首先是先讓讀線程等待,當寫線程寫完執行 Pulse 方法后,喚醒讀線程繼續工作 /// </summary> private static void Read() { while (true) { Monitor.Enter(lockObj);
//讀線程啟動時默認就緒隊列 //讀線程執行到這里時會掛起進入等待隊列 Monitor.Wait(lockObj, 4000); Console.WriteLine("Thread{0} can read nao", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("Thread{0} reading...............", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(3000); Monitor.Exit(lockObj); } } /// <summary> /// 寫數據,當寫線程寫完執行 Pulse 方法后,喚醒讀線程繼續工作 /// </summary> private static void WriteInShareArea() { while (true) { Thread.Sleep(1000); Monitor.Enter(lockObj); Console.WriteLine("Thread{0} change data nao", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("Thread{0} changing...............", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(1000); //喚醒讀線程進入就緒隊列 Monitor.Pulse(lockObj); Monitor.Exit(lockObj); } }
執行結果:
WaitHandle可以說是本章中許多同步對象的基類包括前后文中的AutoResetEvent,ManualResetEvent,mutex 和Semaphore都是其子類,
waitHandle是個抽象類,主要的功能從字面上就能看的出來,等待句柄,不錯 WaitHandle非常的神奇,它容納了一個WIN32的內核對象句柄,線程
會等待內核對象的消息,當然內核對象也會等待接收信號,一旦接收到信號則會通知當前線程,這是一種復雜的操作系統調度原理,大家可以參考
WINDOWS核心編程等書繼續深入了解下,本文旨在說明下waitHandle的概念和一些簡單的介紹,這里在為waitHandle打個比方:
當你想穿過馬路時,如果信號燈為紅色則你只能等待,如果轉變成綠燈的話,你就可以通行,其實waitHandle的作用就是紅綠燈的作用,聰明的的你
肯定想到了,那我們可以將線程同步信號燈放入線程池中么?哈哈當然可以,不僅可以放一根,而且可以放入一堆WaitHadle對象。言歸正傳:WaitHadle
也有2個信號” Signaled” and “NonSignaled” ,前者可以理解為綠燈,綠燈狀態時WaitOne方法無效,當前線程不會被阻止,后者可以理解為紅燈,當底
層內核對象接受到信號時,通知某個當前線程可以進入到共享區(臨界區)其余的必須等待,不過基於抽象類的考慮,waitHandle沒有喚醒線程的set方法,
而讓其子類實現了,所以WaitHandle 還是完全體現其”等啊等,直到提示可以通過或者直接奔潰(超時異常)”的特色
既然waitHandle只有阻塞等待的作用,那我們來看下它的幾個奇妙的方法:
1 bool WaitOne(): 等待一個通過當前waitHandle指定的內核對象收到信號后返回true,否則返回false
2 bool WaitAll():等待waitHandle[]中所有的內核對象都收到信號后返回true,否則返回false
3 bool WaitAny();等待waitHandle[]中內核對象都收到信號后返回true,否則返回false
4 bool SignalAndWait():這個方法無法用文字表達清楚,但是大伙先可以理解成這樣:自動的向內核對象發送信息,等待另一個內核對象接收到信號,
如果另一個內核對象接受到信號則返回TRUE,這牽涉到了復雜的”混合鎖”的機制,所以本文不再詳細說明,后章會詳細介紹混合鎖機制
接着讓我們來看下WaitHandle的幾個派生類結構,后文中將一一介紹:
EventWaitHandle
AutoResetEvent
ManualResetEvent
Mutex
Semaphore (將在下章詳細介紹)
最后上一個圖來表示下WaitHandle的工作原理
大胖子就是個waitHandle(只負責讓當前線程等待,也可以雇佣多個大胖子,內核對象接受到信號后通知大胖子,然后大胖子放人進入club)
4 EventWaitHandle , AutoResetEvent和ManualResetEvent
相信大家對於WaitHandle已經有所了解,接着我們將來介紹下EventWaitHandle ,AutoResetEvent和ManualResetEvent的一些概念和
使用方法(用列表的方式):
EventWaitHandle:
1具有WaitHandle的一些阻塞線程的wait方法
2具有Set方法來釋放被阻塞的當前線程
3具有終止狀態和非終止狀態
4具有自己的重置模式可以選擇:自動重置和手動重置,當eventWaitHandle對象調set方法后,eventWaitHandle會更具重置模式自動重置或手動
重置,重置后會立刻阻塞當前線程(當前線程調用Wait方法后阻塞),如果一直不重置,將無法阻塞當前線程
5 重置模式在初始化eventWaitHandle對象的第二個參數中設置
6 終止狀態不會導致阻塞線程,非終止裝態會導致阻塞當前線程
7 可以調用Reset()方法將狀態設置成非終止裝態來阻塞線程
AutoResetEvent
1 具有父類EventWaitHandle 的功能
2 同樣具有set方法,但是每次只能喚醒一個線程
3 同樣具有終止狀態和非終止狀態
4 重置模式為自動(其實就是一直處於非終止狀態,等待新的線程,waitOne方法將不會阻塞線程,直到新的線程進入臨界區后,自動將模式設置成非
終止模式,阻塞其他線程,如果一直沒有新的線程進入,那只能永遠處於終止狀態)
5 也可以調用Reset()方法將狀態設置成非終止裝態來阻塞線程
6 可以在構造函數中設置默認的狀態(終止狀態和非終止狀態)
簡單示例:
/// <summary> /// 演示waitHandle和AutoResetEvent的同步方法 /// </summary> class Program { static WaitHandle[] waitHandle; static void Main(string[] args) { //委托集合 List<Action> ls = new List<Action> { ()=>{ Console.Write("A");}, ()=>{ Console.Write("B");}, ()=>{ Console.Write("C");}, ()=>{ Console.Write("D");}, ()=>{ Console.Write("E");}, ()=>{ Console.Write("F");}, ()=>{ Console.Write("G");}, }; InvokeAllActions(ls); } /// <summary> /// 利用線程池對委托集合中的所有委托進行觸發,同時通過AutoResetEvent 進行同步 /// </summary> /// <param name="actionList"></param> static void InvokeAllActions(List<Action> actionList) { waitHandle = new WaitHandle[actionList.Count]; if (actionList == null || actionList.Count <= 0) return; int i = 0; actionList.ForEach((action) => { //創建AutoResetEvent對象 waitHandle[i] = new AutoResetEvent(false); //將AutoResetEvent對象作為參數放入線程池 ThreadPool.QueueUserWorkItem(new WaitCallback((handle) => { //觸發委托 action.Invoke(); Thread.Sleep(new Random().Next(1000, 10000)); //委托執行完畢后發出信號,告訴其他線程可以進入共享區 (handle as AutoResetEvent).Set(); }), waitHandle[i]); if (i == actionList.Count - 1) return; i++; }); //注意WaitAll的使用方法,當主線程調用WaitAll方法時,將等待所有的waitHandle接受到信號后才能通過 //信號通過Set()方法發出 WaitHandle.WaitAll(waitHandle); } }
輸出結果可能隨時發生變化
ManualResetEvent
1 具有父類EventWaitHandle 的功能
2 同樣具有set方法,但是每次可以喚醒一個或多個線程
3 同樣具有終止狀態和非終止狀態
4 重置模式為手動(和AutoResetEvent不同的的是執行waitOne方法后,它不會自動將模式設置成非終止模式,也就是說會一直保持有信號狀態,除非
程序員手工寫上Reset方法讓其狀態成為非終止狀態,阻塞其他線程,同理,一旦執行set方法后ManualResetEvent也就可以同時喚醒多個線程繼續執行)
簡單示例:(稍后補上)
Mutext 出現的比monitor更早,而且傳承自COM,當然,waitHandle也是它的父類,它繼承了其父類的功能,有趣的是Mutex的脾氣非常的古怪,它
允許同一個線程多次重復訪問共享區,但是對於別的線程那就必須等待,同時,它甚至支持不同進程中的線程同步,這點更能體現他的優勢,但是劣勢也是顯而
易見的,那就是巨大的性能損耗和容易產生死鎖的困擾,所以除非需要在特殊場合,否則 我們盡量少用為妙,這里並非是將mutex的缺點說的很嚴重,而是建議
大家在適當的場合使用更為適合的同步方式,mutex 就好比一個重量型的工具,利用它則必須付出性能的代價。
接着讓我們了解下mutex的工作方式,同樣我們用列表的方式更簡潔
1. mutex類通過WaitOne 方法阻止另一個線程的進入共享區,但是對於擁有相同的互斥體的線程,WaitOne方法無效,如果其他線程想獲取互斥體的話,必須等
待擁有互斥體的線程執行ReleasMutex方法后釋放該互斥體后才有可能獲取
2. 每次使用WaitOne 方法后都需要使用ReleasMutex方法釋放互斥體的所屬權,waitOne方法和ReleaMutex方法使用次數必須一致,對於遞歸鎖和混合鎖將
非常復雜
3. 同樣具有終止狀態和非終止狀態
4. ReleaMutex 后互斥體的狀態設定為終止,直到其他線程占有互斥體,但是如果沒有線程擁有互斥體的話,該互斥體的狀態便終止了
5. 父類WaitHandle負責接收信號,當接收到信號后方能讓其他線程進入臨界區或是獲取到互斥體的所屬權
6. 能否在進程中互相同步取決於該Mutex對象是否有名字,這似乎有點奇怪,但是大家仔細想下,如果跨進程實現同步的話,那么其他進程假如
也有一些Mutex的吧,那么根本無法告訴在不同進程中的線程是這個互斥體mutex對象負責這方面的同步工作,或許大家會問如果名字一樣怎么辦?
好問題,微軟也想到了,而且它是通過再制造一個新的互斥體替代,這樣和不同進程間的互斥體保持唯一互補沖突,但是這個選項在構造函數中,
下面的構造函數也會闡述這點
7. 對於沒有名字的mutex對象,我們稱之為局部互斥體,相反則是全局互斥體
最后來簡單介紹下Mutex類的幾個重要方法*
1.Bool WaitOne():阻止當前線程,直到收到信號后才能繼續,如果一直沒有接受到信號則永遠不會返回而被阻塞,負責接受信號是比較重要的同步類WaitHandle,
關於這個類,下文中會有詳細闡述
2.void ReleaseMutex()
這個方法非常重要,它是配合waitOne方法而存在的,簡單的說擁有互斥體mutex控制權的線程如果不需要互斥體的話,則使用該方法釋放mutex,再次提醒下
WaitOne方法和ReleaseMutex方法的使用次數必須一致
3. Mutex OpenExisting(string mutexName)
該方法相對於mutex來說也是必不可少,它的作用是查找mutex是否存在,如果不存在的話,則會引發WaitHandleCannotBeOpenedException,我們可以再這
個捕獲異常中實現mutex對象的創建
Mutex類的構造方法:
1.Mutex()
用無參數的構造函數得到的Mutex沒有任何名稱,而進程間無法通過變量的形式共享數據,所以沒有名稱的Mutex也叫做局部(Local)Mutex。另外,
這樣創建出的Mutex,創建者對這個實例並沒有擁有權,仍然需要調用WaitOne()去請求所有權。
2.Mutex(Boolean initiallyOwned)
與上面的構造函數一樣,它只能創建沒有名稱的局部Mutex,無法用於進程間的同步。Boolean參數用於指定在創建者創建Mutex后,
是否立刻獲得擁有權,因此Mutex(false)等效於Mutex()。
3.Mutex(Boolean initiallyOwned, String name)
在這個構造函數里我們除了能指定是否在創建后獲得初始擁有權外,還可以為這個Mutex取一個名字。只有這種命名的Mutex才可以被其它應用程序域中的程序所使用,因此
這種Mutex也叫做全局(Global)Mutex。 如果String為null或者空字符串,那么這等同於創建一個未命名的Mutex。因為可能有其他程序先於你創建了同名的Mutex,
因此返回的 Mutex實例可能只是指向了同名的Mutex而已。但是,這個構造函數並沒有任何機制告訴我們這個情況。因此,如果要創建一個命名的Mutex,並且期 望知道這
個Mutex是否由你創建,最好使用下面兩個構造函數中的任意一個。最后,請注意name是大小寫敏感的。
4.Mutex(Boolean initiallyOwned, String name, out Boolean createdNew):
頭兩個參數與上面的構造函數相同,第三個out參數用於表明是否獲得了初始的擁有權。這個構造函數應該是我們在實際中使用較多的。
5.Mutex(Boolean initiallyOwned, String name, out Booldan createdNew, MutexSecurity):
多出來的這個MutexSecurity參數,也是由於全局Mutex的特性所決定的。
同樣馬上上一個例子:
/// <summary> /// 本示例通過多線程拷貝字符數組 /// </summary> class Program { private static char[] testChars = "abcdefg".ToCharArray(); private static char[] backChars = new char[testChars.Length]; private static Mutex mutex = new Mutex(false,"myMutex"); static void Main(string[] args) { try { //尋找名為myMutex2的互斥體對象 Mutex mutex2= Mutex.OpenExisting("myMutex2", MutexRights.FullControl); } catch (WaitHandleCannotBeOpenedException e) { Console.WriteLine("the name of mutex is not exist, error message:{0}",e.Message); } for (int i = 0; i < testChars.Length; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(SetBackString)); } Console.ReadLine(); } static void SetBackString(object state=null) { ///阻止當前線程,直到收到信號后才能繼續 mutex.WaitOne(); try { if (testChars.Length > 0) { //每次將test4的第一個字符串放入backChars,原始backChars.Length 減去被刪除一個char的testChars.length Array.Copy(testChars, 0, backChars, backChars.Length - testChars.Length, 1); Console.WriteLine("ThreadID:{0} the chart:{1} will insert into backChars,the string changed from backChars:{2}", Thread.CurrentThread.ManagedThreadId, testChars[0], string.Join(string.Empty, backChars)); //申明一個臨時char數組用來存放testChars,實際作用是刪除testChars的一個char char[] temp = new char[testChars.Length-1]; //將不需要刪除的chars拷貝入temp Array.Copy(testChars,1, temp, 0, testChars.Length-1); testChars = temp; } } catch { } finally { //無論發生什么當前線程最終還是得釋放互斥體的控制權 mutex.ReleaseMutex(); } } }
顯示結果:
相信大家看了上文后對於的Mutex和Monitor已經有所了解,由於這2個工具類可能用法上過於相似,當然區別也是不小
1. Monitor不是waitHandle的子類,它具有等待和就緒隊列的實際應用
2 Monitor無法跨進程中實現線程同步,但是Mutex可以
3 相對而言兩者有明顯的性能差距,mutex相對性能較弱但是功能更為強大,monitor則性能比較好
4 兩者都是用鎖的概念來實現同步不同的是monitor一般在方法(函數)調用方加鎖;mutex一般在方法(函數)內部加鎖,即鎖定被調用端
以上是比較主要的區別,一些細節大家也可以繼續深究
有時我們苦於在項目中尋找適合的同步工具,所以一下僅是個人觀點,大伙可以討論或者發表自己的意見
1 處於性能要求考慮:可以考慮用基元用戶模式的同步工具,也就是前一篇中的一些同步工具,盡量不要考慮mutex因為其功能強大所以性能損失太多
2 處於功能考慮:如果項目中牽涉到復雜的同步而且不需要嚴格的性能要求,例如跨進程,混合鎖或者遞歸鎖等等,則最好選擇基元內核模式中的同步工具
3 分布式開發,在各個服務間實現同步的話,當然mutex是第一考慮
4 中小型項目大家可以隨意,根據業務需要決定
本章介紹了monitor,waitHandle 及其一些子類包括EventWaitHandle,AutoResetEvent,ManualResetEvent和mutex, 關於Semaphore 會在下章介紹,
不僅如此,下章還會介紹混合鎖和遞歸鎖,盡請期待!最后謝謝女友背后默默背后的支持,謝謝大家,還請大家能夠支持推薦下