文章原始出處 http://xxinside.blogbus.com/logs/47523285.html
預備知識:C#線程同步(1)- 臨界區&Lock,C#線程同步(2)- 臨界區&Monitor,C#線程同步(3)- 互斥量 Mutex
WaitHandle一家
在前一篇我們已經提到過Mutex和本篇的主角們直接或間接繼承自WaitHandle:
- Mutex類,這個我們在上一篇已經講過。
- EventWaitHandle 類及其派生類AutoResetEvent 和 ManualResetEvent,這是本篇的主角。
- Semaphore 類,即信號量,我們下一篇再講。
WaitHandle提供了若干用於同步的方法。上一篇關於Mutex的blog中已經講到一個WaitOne(),這是一個實例方法。除此之外,WaitHandle另有3個用於同步的靜態方法:
- SignalAndWait(WaitHandle, WaitHandle):以原子操作的形式,向第一個WaitHandle發出信號並等待第二個。即喚醒阻塞在第一個WaitHandle上的線程/進程,然后自己等待第二個WaitHandle,且這兩個動作是原子性的。跟WaitOne()一樣,這個方法另有兩個重載方法,分別用Int32或者TimeSpan來定義等待超時時間,以及是否從上下文的同步域中退出。
- WaitAll(WaitHandle[]):這是用於等待WaitHandle數組里的所有成員。如果一項工作,需要等待前面所有人完成才能繼續,那么這個方法就是一個很好的選擇。仍然有兩個用於控制等待超時的重載方法,請自行參閱。
- WaitAny(WaitHandle[]):與WaitAll()不同,WaitAny只要等到數組中一個成員收到信號就會返回。如果一項工作,你只要等最快做完的那個完成就可以開始,那么WaitAny()就是你所需要的。它同樣有兩個用於控制等待超時的重載。
線程相關性(Thread Affinity )
EventWaitHandle和Mutex兩者雖然是派生自同一父類,但有着完全不同的線程相關性:
- Mutex與Monitor一樣,是“線程相關(Thread Affinity)”的。我們之前已經提到過,只有通過Monitor.Enter()/TryEnter()獲得對象鎖的線程才能調用Pulse()/Wait()/Exit();同樣的,只有獲得Mutex擁有權的線程才能執行ReleaseMutex()方法,否則就會引發異常。這就是所謂的線程相關性。
- 相反,EventWaitHandle以及它的派生類AutoResetEvent和ManualResetEvent都是線程無關的。任何線程都可以發信號給EventWaitHandle,以喚醒阻塞在上面的線程。
- 下一篇要提到的Semaphore也是線程無關的。
Mutex與Event
我們在Mutex一篇中沒有具體提到Mutex是否能發送信號,只是簡單說Mutex不太適合有相互消息通知的同步,它僅有的一些同步方法是來自其父類的靜態方法。那么現在我們可以仔細來看看Mutex到底能不能用於關於Monitor那篇提到的生產者、消費者和糖罐的場景。
回過頭來仔細查看Mutex的所有方法,除了一個我們已經提到的WaitHandle上的靜態方法SingnalAndWait(toSingnal, toWaitOn),我們找不到任何“屬於Mutex自己”的、用於發送信號的方法。退而求其次吧,我們就來看看這個靜態方法是否可以讓Mutex具有通知的能力。
如果toSignal是一個Mutex,那么收到“信號”就等效於ReleaseMutex()。而由於Mutex的線程相關性,只有擁有當前Mutex的線程才能夠發送這個信號(ReleaseMutex),否則會引發異常。也就是說如果要用這個方法來通知其它線程同步,Mutex只能自己發給自己。與之相反,如果第二個參數toWaitOn也是個Mutex,那么這個Mutex不能是自己。因為前篇已經講過,Mutex的擁有者可以多次WaitOne()而不阻塞,這里也是一樣。所以如果Mutex一定要使用這個方法,准確的說是只是成為這個方法的參數,那只能是WaitHandle.SignalAndWait(它自己,另一個Mutex)。
試想,如果有人試圖只使用Mutex來進行同步通知。假設生產者線程通過Mutex上的WaitOne()獲得了mutexA的擁有權,並且在生產完畢后調用了SingnalAndWait(mutexA,mutexB),通知由於當前mutexA而阻塞的消費者線程,並且將自己阻塞在mutexB上。那么被喚醒的消費者線程獲得MutexA的擁有權吃掉糖后,也只能調用SingnalAndWait(mutexA,mutexB)釋放它獲得的mutexA且阻塞於MutexB。問題來了,此時的生產者是阻塞在mutexB上……也許,我們可以設計一段“精巧”的代碼,讓生產者和消費者一會兒阻塞在mutexA,一會兒阻塞在mutexB上……我不想花費這個力氣去想了,你可以試試看:)。不管有沒有這樣的可能,Mutex很明顯就不適用於通知的場景。
EventWaitHandle的獨門秘笈
正因為Mutex沒有很好地繼承父輩的衣缽,EventWaitHandle以及它的兒子/女兒們便來到了這個世界上。
EventWaitHandle、AutoResetEvent、ManualResetEvent名字里都有一個“Event”,不過這跟.net的本身的事件機制完全沒有關系,它不涉及任何委托或事件處理程序。相對於我們之前碰到的Monitor和Mutex需要線程去爭奪“鎖”而言,我們可以把它們理解為一些需要線程等待的“事件”。線程通過等待這些事件的“發生”,把自己阻塞起來。一旦“事件”完成,被阻塞的線程在收到信號后就可以繼續工作。
為了配合WaitHandle上的3個靜態方法SingnalAndWait()/WailAny()/WaitAll(),EventWaitHandle提供了自己獨有的,使“Event”完成和重新開始的方法:
- bool:Set():英文版MSDN:Sets the state of the event to signaled, allowing one or more waiting threads to proceed;中文版MSDN:將事件狀態設置為終止狀態,允許一個或多個等待線程繼續。初看“signaled”和“終止”似乎並不對應,細想起來這兩者的說法其實也不矛盾。事件如果在進行中,當然就沒有“終止”,那么其它線程就需要等待;一旦事件完成,那么事件就“終止”了,於是我們發送信號喚醒等待的線程,所以“信號已發送”狀態也是合理的。兩個小細節:
- 無論中文還是英文版,都提到這個方法都是可以讓“一個”或“多個”等待線程“繼續/Proceed”(注意不是“喚醒”)。所以這個方法在“喚醒”這個動作上是類似於Monitor.Pulse()和Monitor.PulseAll()的。至於什么時候類似Pulse(),又在什么時候類似PulseAll(),往下看。
- 這個方法有bool型的返回值:如果該操作成功,則為true;否則,為false。不過MSDN並沒有告訴我們,什么時候執行會失敗,你只有找個微軟MVP問問了。
- bool:Reset():Sets the state of the event to nonsignaled, causing threads to block. 將事件狀態設置為非終止狀態,導致線程阻止。 同樣,我們需要明白“nonsignaled”和“非終止”是一回事情。還同樣的是,仍然有個無厘頭的返回值。Reset()的作用,相當於讓事件重新開始處於“進行中”,那么此后所有WaitOne()/WaitAll()/WaitAny()/SignalAndWait()這個事件的線程都會再次被擋在門外。
來看看EventWaitHandle眾多構造函數中最簡單的一個:
- EventWaitHandle(Boolean initialState, EventResetMode mode):初始化EventWaitHandle類的新實例,並指定等待句柄最初是否處於終止狀態,以及它是自動重置還是手動重置。大多數時候我們會在第一個參數里使用false,這樣新實例會缺省為“非終止”狀態。第二個參數EventResetMode是一個枚舉,一共兩個值:
- EventResetMode.AutoReset:當Set()被調用當前EventWaitHandle轉入終止狀態時,若有線程阻塞在當前EventWaitHandle上,那么在釋放一個線程后EventWaitHandle就會自動重置(相當於自動調用Reset())再次轉入非終止狀態,剩余的原來阻塞的線程(如果有的話)還會繼續阻塞。如果調用Set()后本沒有線程阻塞,那么EventWaitHandle將保持“終止”狀態直到一個線程嘗試等待該事件,這個該線程不會被阻塞,此后EventWaitHandle才會自動重置並阻塞那之后的所有線程。
- EventResetMode.ManualReset:當終止時,EventWaitHandle 釋放所有等待的線程,並在手動重置前,即Reset()被調用前,一直保持終止狀態。
好了,現在我們可以清楚的知道Set()在什么時候分別類似於Monitor.Pulse()/PulseAll()了:
- 當EventWaitHandle工作在AutoReset模式下,就喚醒功能而言,Set()與Monitor.Pulse()類似。此時,Set()只能喚醒眾多(如果有多個的話)被阻塞線程中的一個。但兩者仍有些差別:
- Set()的作用不僅僅是“喚醒”而是“釋放”,可以讓線程繼續工作(proceed);相反,Pulse()喚醒的線程只是重新進入Running狀態,參與對象鎖的爭奪,誰都不能保證它一定會獲得對象鎖。
- Pulse()的已被調用的狀態不會被維護。因此,如果在沒有等待線程時調用Pulse(),那么下一個調用Monitor.Wait()的線程仍然會被阻塞,就像Pulse() 沒有被被調用過。也就是說Monitor.Pulse()只在調用當時發揮作用,並不象Set()的作用會持續到下一個WaitXXX()。
- 在一個工作在ManualReset模式下的EventWaitHandle的Set()方法被調用時,它所起到的喚醒作用與Monitor.PulseAll()類似,所有被阻塞的線程都會收到信號被喚醒。而兩者的差別與上面完全相同。
來看看EventWaitHandle的其它構造函數:
- EventWaitHandle(Boolean initialState, EventResetMode mode, String name):頭兩個參數我們已經看過,第三個參數name用於在系統范圍內指定同步事件的名稱。是的,正如我們在Mutex一篇中提到的,由於父類WaitHandle是具有跨進程域的能力的,因此跟Mutex一樣,我們可以創建一個全局的EventWaitHandle,讓后將它用於進程間的通知。注意,name仍然是大小寫敏感的,仍然有命名前綴的問題跟,你可以參照這里。當name為null或空字符串時,這等效於創建一個局部的未命名的EventWaitHandle。仍然同樣的還有,可能會因為已經系統中已經有同名的EventWaitHandle而僅僅返回一個實例表示同名的EventWaitHandle。所以最后仍舊同樣地,如果你需要知道這個EventWaitHandle是否由你最先創建,你需要使用以下兩個構造函數之一。
- EventWaitHandle(Boolean initialState, EventResetMode mode, String name, out Boolean createdNew):createdNew用於表明是否成功創建了EventWaitHandle,true表明成功,false表明已經存在同名的事件。
- EventWaitHandle(Boolean initialState, EventResetMode mode, String name, out Boolean createdNew, EventWaitHandleSecurity):關於安全的問題,直接查看這個構造函數上的例子吧。全局MutexEventWaitHandle的安全問題應該相對Mutex更需要注意,因為有可能黑客程序用相同的事件名對你的線程發送信號或者進行組織,那樣可能會嚴重危害你的業務邏輯。
好啦,都差不多了,可以寫一個例子試試了。讓我們回到Monitor一篇中提到的生產者和消費者場景,讓我們看看EventWaitHandle能不能完成它兄弟Mutex沒有能完成的事業。不過,即便有強大通信能力的EventWaitHandle出馬,也避免不要使用lock/monitor或是Mutex。原因很簡單,糖罐是一個互斥資源,必須被互斥地訪問。而EventWaitHanldle跟Mutex相反,能通信了但卻完全失去了臨界區的能力。所以,這個例子其實並不太適合展示EventWaitHandle的通信機制,我只是為了想用同樣的例子來比較這些同步機制間的差異。
EventWaitHandle雖然還必須借助lock/Monitor/Mutex來實現這個例子(僅僅是臨界區部分),但是它終究有強於Monitor的通信能力,所以讓我們來擴展一下這個例子:現在有一個生產者,有多個消費者。
- 我們讓消費者在沒有糖吃或吃完一塊糖后阻塞在一個工作在ManualReset模式下的EventWaitHandle,生產者在生產完畢后就通過這個事件喚醒所有消費者吃糖。由於我們使用了lock的關系,雖然所有消費者都被喚醒,但是他們還是因為爭奪糖罐的關系只有一個能進入臨界區吃糖。不過此時阻塞的原因並不是因為我們的通知時間,而是臨界區的問題。
- 每個消費者有一條專線,即一個工作在AutoRest模式下的EventWaitHandle,用於在吃完糖后通知生產者。而生產者用WaitAny()來等待消費者吃糖時間的發生,只要有任一消費者吃完糖,那么生產者就試圖爭奪對糖罐的擁有權,把糖罐塞滿(一人一顆的標准)。消費者這里使用了WaitAndSignal給生產者發消息,並等待生產者進入臨界區生產糖后通知他們。在這樣的設計邏輯下,可能糖罐中的糖還沒有全部吃完生產者就有機會再次把糖罐裝滿。當然,你也可以使用了WaitAll()來等待所有消費者吃完再進行生產。
using System; using System.Collections; using System.Linq; using System.Text; using System.Threading;
class WaitEventHandleSample:IDisposable { private volatile bool _shouldStop = false; //用於控制線程正常結束的標志 private const int _numberOfConsumer = 5; //消費者的數目 //容器,一個只能容納一塊糖的糖盒子。PS:現在MS已經不推薦使用ArrayList, //支持泛型的List才是應該在程序中使用的,我這里偷懶,不想再去寫一個Candy類了。 private ArrayList _candyBox = null;
private EventWaitHandle _EvntWtHndlProduced = null; //生產完成的事件,ManualReset,用於通知所有消費者生產完成 private EventWaitHandle[] _EvntWtHndlConsumed = null; //消費完成的事件,AutoReset,每一個消費線程對應一個事件,用於通知生產者有消費動作完成
/// <summary> /// 用於結束Produce()和Consume()在輔助線程中的執行 /// </summary> public void StopThread() { _shouldStop = true; //叫醒阻塞中的消費者,讓他們看到線程結束標志 if (_EvntWtHndlProduced != null) { _EvntWtHndlProduced.Set(); }; //叫醒阻塞中的生產者,讓他看到線程結束標志 if (_EvntWtHndlConsumed != null) { for (int i = 0; i < _numberOfConsumer; i++) { if (_EvntWtHndlConsumed[i] != null) { _EvntWtHndlConsumed[i].Set(); }; } } }
/// <summary> /// 生產者的方法 /// </summary> public void Produce() { if (_candyBox == null) { Console.WriteLine("生產者:糖罐在哪里?!"); } else if (_EvntWtHndlConsumed == null) { Console.WriteLine("生產者:消費者們在哪里?!"); } else if (_EvntWtHndlProduced == null) //這個事件用於喚醒所有消費者,因此象個喇叭 { Console.WriteLine("生產者:喇叭壞啦,沒辦法通知消費者!"); } else { //逐一檢查消費者是否到位 for (int i = 0; i < _numberOfConsumer; ++i) { if (_EvntWtHndlConsumed[i] == null) { Console.WriteLine("生產者:消費者{0}在哪里?!", i); return; } else { //什么也不做 }; }; int numberOfSugarProduced = 0; //本次一共生產了多少顆糖
while (!_shouldStop) { lock (_candyBox) { if (_candyBox.Count < _numberOfConsumer) { numberOfSugarProduced = 0; while (_candyBox.Count < _numberOfConsumer) //一共有多少個消費者就生產多少塊糖 { //生產一塊糖 _candyBox.Add("A Candy"); ++numberOfSugarProduced; }; Console.WriteLine("生產者:這次生產了{0}塊糖,罐里現在一共有{1}塊糖!", numberOfSugarProduced, _candyBox.Count); Console.WriteLine("生產者:趕快來吃!!"); } else //容器是滿的 { Console.WriteLine("生產者:糖罐是滿的!"); }; }; //通知消費者生產已完成 _EvntWtHndlProduced.Set(); //只要有消費者吃完糖,就開始生產 EventWaitHandle.WaitAny(_EvntWtHndlConsumed); Thread.Sleep(2000); }; Console.WriteLine("生產者:下班啦!"); } }
/// <summary> /// 消費者的方法 /// </summary> /// <param name="consumerIndex">消費者序號,用於表明使用哪個_EvntWtHndlConsumed成員</param> public void Consume(object consumerIndex) { int index = (int)consumerIndex; if (_candyBox == null) { Console.WriteLine("消費者{0}:糖罐在哪里?!",index); } else if (_EvntWtHndlProduced == null) { Console.WriteLine("消費者{0}:生產者在哪里?!",index); } else if (_EvntWtHndlConsumed == null || _EvntWtHndlConsumed[index] == null) { Console.WriteLine("消費者{0}:電話壞啦,沒辦法通知生產者!", index); //由於每個消費者都有一個專屬事件通知生產者,因此相當於電話 } else { while (!_shouldStop || _candyBox.Count > 0) //即便看到結束標致也應該把容器中的所有資源處理完畢再退出,否則容器中的資源可能就此丟失。需要指出_candybox.Count是有可能讀到臟數據的 { lock (_candyBox) { if (_candyBox.Count > 0) { if (!_shouldStop) { _candyBox.RemoveAt(0); Console.WriteLine("消費者{0}:吃了1顆糖,還剩{1}顆!!", index, _candyBox.Count); Console.WriteLine("消費者{0}:趕快生產!!",index); } else { Console.WriteLine("消費者{0}:我來把剩下的糖都吃了!",index); while (_candyBox.Count > 0) { _candyBox.RemoveAt(0); Console.WriteLine("消費者{0}:吃了1顆糖,還剩{1}顆!!", index, _candyBox.Count); } break; } } else { Console.WriteLine("消費者{0}:糖罐是空的!",index); Console.WriteLine("消費者{0}:趕快生產!!",index); } } WaitHandle.SignalAndWait(_EvntWtHndlConsumed[index], _EvntWtHndlProduced); Thread.Sleep((index+1)*1500); } } Console.WriteLine("消費者{0}:都吃光啦,下次再吃!",index); }
/// <summary> /// 初始化所需的各EventWaitHandle和糖罐等 /// </summary> public void Initialize() { if (_candyBox == null) { _candyBox = new ArrayList(_numberOfConsumer); //按有多少消費者最多生產多少糖的標准初始化糖罐大小 } else { //什么也不做 }
if (_EvntWtHndlProduced == null) { _EvntWtHndlProduced = new EventWaitHandle(false, EventResetMode.ManualReset); } else { //什么也不做 }
if (_EvntWtHndlConsumed == null) { _EvntWtHndlConsumed = new EventWaitHandle[_numberOfConsumer]; for (int i = 0; i < _numberOfConsumer; ++i) { _EvntWtHndlConsumed[i] = new EventWaitHandle(false, EventResetMode.AutoReset); } } else { //什么也不做 } }
static void Main(string[] args) { WaitEventHandleSample ss = new WaitEventHandleSample(); try { ss.Initialize();
//Start threads. Console.WriteLine("開始啟動線程,輸入回車終止生產者和消費者的工作……\r\n******************************************");
Thread thdProduce = new Thread(new ThreadStart(ss.Produce)); thdProduce.Start();
Thread[] thdConsume = new Thread[_numberOfConsumer]; for (int i = 0; i < _numberOfConsumer; ++i) { thdConsume[i] = new Thread(new ParameterizedThreadStart(ss.Consume)); thdConsume[i].Start(i); }
Console.ReadLine(); //通過IO阻塞主線程,等待輔助線程演示直到收到一個回車 ss.StopThread(); //正常且優雅的結束生產者和消費者線程
thdProduce.Join();
for (int i = 0; i < _numberOfConsumer; ++i) { thdConsume[i].Join(); } Console.WriteLine("******************************************\r\n輸入回車結束!"); Console.ReadLine(); } finally { ss.Dispose(); ss = null; }; }
#region IDisposable Members public void Dispose() { if (_candyBox != null) { _candyBox.Clear(); _candyBox = null; } else { //什么也不做 }
if (_EvntWtHndlProduced != null) { _EvntWtHndlProduced.Set(); _EvntWtHndlProduced.Close(); _EvntWtHndlProduced = null; } else { //什么也不做 }
if (_EvntWtHndlConsumed != null) { for (int i = 0; i < _numberOfConsumer; ++i) { if (_EvntWtHndlConsumed[i] != null) { _EvntWtHndlConsumed[i].Set(); _EvntWtHndlConsumed[i].Close(); _EvntWtHndlConsumed[i] = null; }; } _EvntWtHndlConsumed = null; } else { //什么也不做 }; } #endregion }
Produce()和Consum()中加入的Sleep代碼僅僅是為了讓線程更為隨機的被調度,這樣我們可以更容易觀察到線程亂序執行的情況。另外,如果是一個需要跨進程同步的程序,那么你也可以用Mutext替換lock實現臨界區。下面是某次執行的輸出情況,你的結果當然會跟它不同(空行位置是我輸入回車終止線程的時機):
開始啟動線程,輸入回車終止生產者和消費者的工作…… ****************************************** 生產者:這次生產了5塊糖,罐里現在一共有5塊糖! 生產者:趕快來吃!! 消費者0:吃了1顆糖,還剩4顆!! 消費者0:趕快生產!! 消費者1:吃了1顆糖,還剩3顆!! 消費者1:趕快生產!! 消費者2:吃了1顆糖,還剩2顆!! 消費者2:趕快生產!! 消費者3:吃了1顆糖,還剩1顆!! 消費者3:趕快生產!! 消費者4:吃了1顆糖,還剩0顆!! 消費者4:趕快生產!! 消費者0:糖罐是空的! 消費者0:趕快生產!! 生產者:這次生產了5塊糖,罐里現在一共有5塊糖! 生產者:趕快來吃!! 消費者1:吃了1顆糖,還剩4顆!! 消費者1:趕快生產!! 消費者0:吃了1顆糖,還剩3顆!! 消費者0:趕快生產!! 生產者:這次生產了2塊糖,罐里現在一共有5塊糖! 生產者:趕快來吃!! 消費者0:吃了1顆糖,還剩4顆!! 消費者0:趕快生產!! 消費者2:吃了1顆糖,還剩3顆!! 消費者2:趕快生產!! 消費者1:吃了1顆糖,還剩2顆!! 消費者1:趕快生產!! 生產者:這次生產了3塊糖,罐里現在一共有5塊糖! 生產者:趕快來吃!! 消費者0:吃了1顆糖,還剩4顆!! 消費者0:趕快生產!! 消費者3:吃了1顆糖,還剩3顆!! 消費者3:趕快生產!! 消費者4:吃了1顆糖,還剩2顆!! 消費者4:趕快生產!! 消費者0:吃了1顆糖,還剩1顆!! 消費者0:趕快生產!!
生產者:下班啦! 消費者1:我來把剩下的糖都吃了! 消費者1:吃了1顆糖,還剩0顆!! 消費者1:都吃光啦,下次再吃! 消費者0:都吃光啦,下次再吃! 消費者2:都吃光啦,下次再吃! 消費者3:都吃光啦,下次再吃! 消費者4:都吃光啦,下次再吃! ****************************************** 輸入回車結束!
AutoResetEvent & ManuResetEvent
到此為止我們還沒有提到過EventWaitHandle的這兩個兒子,不過這就是一兩句話的事:
- AutoResetEvent在功能上等效於用EventResetMode.AutoReset 創建的未命名的 EventWaitHandle。
- ManualResetEvent在功能上等效於用EventResetMode.ManualReset 創建的未命名的 EventWaitHandle。
好了,講這么都就夠了,這兩個子類無非是為了方便使用而存在的。不過請記得這兩個子類永遠是局部/Local的,並不能象它們的父類一樣用於進程間的通信。
還是給出一個簡單的例子,這個例子只跟通知有關,不再涉及臨界資源。假設一個跑步比賽的場景,我們用一個ManualResetEvent表示比賽,然后為每個運動員配備一個AutoResetEvent用於通知到起跑線或者是達終點。首先運動員需要到起跑線上就位,這個過程我們讓運動員到達起跑線后調用AutoResetEvent上的Reset()發出信號,同時使用ManualResetEvent上的WaitOne()阻塞自己准備起跑。另一方面,我們在比賽線程上先用WaitHandle.WaitAll(AutoResetEvent[])等待所有運動員到位。WaitAll()完成后,使用ManualResetEvent上的Reset()發令開始比賽,再使用WaitHandle.WaitAny(AutoResetEvent[])等待第一個運動員沖線。而每個運動員到終點后會再次調用AutoResetEvent.Reset()表示到達。
using System; using System.Threading; using System.Linq; using System.Text;
class Runner : IDisposable { //用於讓所有運動員到達起跑線准備起跑 private ManualResetEvent _mnlRstEvntStartLine = null; //用於運動員到達終點時發出信號 private static AutoResetEvent[] _mnlRstEvntRunner = null; private const int _numberOfRunner = 8;
private Random _rnd = new Random(); /// <summary> /// 構造函數 /// </summary> public Runner() { _mnlRstEvntStartLine = new ManualResetEvent(false); _mnlRstEvntRunner = new AutoResetEvent[_numberOfRunner]; //請運動員就位 for (int i = 0; i < _numberOfRunner; ++i) { _mnlRstEvntRunner[i] = new AutoResetEvent(false); } }
/// <summary> /// 運動員方法 /// </summary> /// <param name="id">運動員序號</param> public void Run(object id) { int index = (int)id;
//等待信號准備起跑 Console.WriteLine("{0}號運動員就位。", index); _mnlRstEvntRunner[index].Set();
//等待發令 _mnlRstEvntStartLine.WaitOne();
//隨機睡眠,表示不同運動員跑的快慢 Thread.Sleep(_rnd.Next(2000));
Console.WriteLine("{0}號運動員到達終點!", index); _mnlRstEvntRunner[index].Set(); }
/// <summary> /// 比賽開始 /// </summary> public void Start() { Thread[] runners = new Thread[_numberOfRunner];
//請運動員就位 for (int i = 0; i < _numberOfRunner; ++i) { runners[i] = new Thread(Run); runners[i].Start(i); } //等待所有運動員就位 WaitHandle.WaitAll(_mnlRstEvntRunner);
//發令起跑 Console.WriteLine("***********************起跑!!!*************************"); _mnlRstEvntStartLine.Set();
//看看誰先到達終點 int index = WaitHandle.WaitAny(_mnlRstEvntRunner);
//等待所有運動員到達終點 //請運動員就位 for (int i = 0; i < _numberOfRunner; ++i) { runners[i].Join(); } Console.WriteLine("**********************************************************"); Console.WriteLine("{0}號運動員奪得冠軍!", index); Console.WriteLine("***********************比賽結束***************************"); }
static void Main() { Runner ss = new Runner(); try { ss.Start(); } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { ss.Dispose(); ss = null; Console.WriteLine("輸入回車結束"); Console.ReadLine(); } }
#region IDisposable Members
public void Dispose() { if (_mnlRstEvntStartLine != null) { _mnlRstEvntStartLine.Set(); _mnlRstEvntStartLine.Close(); } else { //do nothing }
if (_mnlRstEvntRunner != null) { for (int i = 0; i < _numberOfRunner; ++i) { if (_mnlRstEvntRunner[i] != null) { _mnlRstEvntRunner[i].Set(); _mnlRstEvntRunner[i].Close(); _mnlRstEvntRunner[i] = null; } else { //do nothing } } _mnlRstEvntRunner = null; } } #endregion }
可能的執行結果:
0號運動員就位。 1號運動員就位。 2號運動員就位。 3號運動員就位。 4號運動員就位。 5號運動員就位。 6號運動員就位。 7號運動員就位。 ***********************起跑!!!************************* 3號運動員到達終點! 1號運動員到達終點! 0號運動員到達終點! 4號運動員到達終點! 2號運動員到達終點! 5號運動員到達終點! 6號運動員到達終點! 7號運動員到達終點! ********************************************************** 3號運動員奪得冠軍! ***********************比賽結束***************************
題外話:派生總是優雅的嗎? 在WaitHandle家族這個繼承關系里,我實在忍不住要說“丑陋”兩個字。Mutex以及下篇將要講到的信號量Semaphore,實在是太委屈地接受了來自WaitHandle上不相關的靜態方法。WaitAll(),WaitAny(),SignalAndWait()完完全全就是為EventWaitHandle這一族定制的。繼承本來想體現的多態性,也僅僅是體現在這幾個方法的參數是WaitHandle上,不過有誰會真的在這幾個方法上使用Mutex或者Semaphore實例呢?也許Mutex和Semaphore是WaitHandle“抱養”的吧,否則它怎么這么偏心?:) Mutex與EventWaitHandle完全是站在同步的兩個方向:Mutex是“鎖”可以實現互斥訪問但幾乎不具有通信能力;而EventWaitHandle有強大的通信能力,但卻不能實現對資源的互斥訪問。從一個父類,派生出兩個有如此大差異的子類實在不知道是為何。從這種意義上來講,似乎Monitor比較“全面”,兩邊都能做一點。 在基礎類庫里出現這樣的狀況,似乎確實無法對此表示信服(這可能是有些Java程序員鄙視.Net一脈的原因之一吧,Java在語言規范和OO理論上的優雅的確有些讓人着迷:))。不過,我們還是要體諒一下MS。它的產品線是那么龐大,產品生命周期是那么持久,你不可能期望Windows API剛出現的時候就能夠為.Net未來的優雅考慮。一代代的更替中,他們總需要面對之前實現的一些限制。畢竟這幾個類的根源是比較直接地對Win32 API地封裝。