C#线程同步(4)- 通知&EventWaitHandle一家
文章原始出处 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地封装。
C#线程同步(4)- 通知&EventWaitHandle一家的更多相关文章
- C#线程同步(5)- 信号量 Semaphore
文章原始出处 http://xxinside.blogbus.com/logs/47617134.html 预备知识:C#线程同步(1)- 临界区&Lock,C#线程同步(2)- 临界区&am ...
- C#中的线程(二) 线程同步基础
1.同步要领 下面的表格列展了.NET对协调或同步线程动作的可用的工具: 简易阻止方法 构成 目的 Sleep 阻止给定的时间周期 Join 等待另一个线程 ...
- C#中的几个线程同步对象方法
在编写多线程程序时无可避免会遇到线程的同步问题.什么是线程的同步呢? 举个例子:如果在一个公司里面有一个变量记录某人T的工资count=100,有两个主管A和B(即工作线程)在早一些时候拿了这个变量的 ...
- C#中的线程(中)-线程同步
1.同步要领 下面的表格列展了.NET对协调或同步线程动作的可用的工具: 简易阻止方法 构成 目的 Sleep 阻止给定的时间周期 Join 等待另一个线程 ...
- .net中的线程同步基础(搬运自CLR via C#)
线程安全 此类型的所有公共静态(Visual Basic 中为 Shared)成员对多线程操作而言都是安全的.但不保证任何实例成员是线程安全的. 在MSDN上经常会看到这样一句话.表示如果程序中有n个 ...
- C#线程同步(3)- 互斥量 Mutex
文章原始出处 http://xxinside.blogbus.com/logs/47162540.html 预备知识:C#线程同步(1)- 临界区&Lock,C#线程同步(2)- 临界区&am ...
- 线程同步 –AutoResetEvent和ManualResetEvent
上一篇介绍了通过lock关键字和Monitor类型进行线程同步,本篇中就介绍一下通过同步句柄进行线程同步. 在Windows系统中,可以使用内核对象进行线程同步,内核对象由系统创建并维护.内核对象为内 ...
- C#中的线程(二)线程同步
C#中的线程(二)线程同步 Keywords:C# 线程Source:http://www.albahari.com/threading/Author: Joe AlbahariTranslato ...
- 第二十篇 .NET高级技术之C#中的线程(二) 线程同步基础
1.同步要领 下面的表格列展了.NET对协调或同步线程动作的可用的工具: 简易阻止方法 构成 目的 Sleep 阻止给定的时间周期 Join 等待另一个线程 ...
随机推荐
- Linux ,Ubuntu 分区建议大小
分区 分区类型 文件系统 大小 /boot 逻辑分区 Ext4 300M swap 逻辑分区 交换空间 13G / 主分区 Ext4 30G /home 逻辑分区 Ext4 42G /usr 逻辑分区 ...
- 实现promise
// 判断变量否为function const isFunction = variable => typeof variable === 'function' // 定义Promise的三种 ...
- 20175303 2018-2019-2 《Java程序设计》第8周学习总结
20175303 2018-2019-2 <Java程序设计>第8周学习总结 教材学习内容总结 本周学习<Java程序设计>第十五章: 泛型: 泛型(Generics)的主要目 ...
- VUE 安装&创建一个项目
1,安装node.js vue依赖nodejs,所以首先要安装node.js 然后打开cmd,输入命令, node -v.正常出现版本号,说明你已经安装成功了 下载地址:http://nodejs.c ...
- highcharts-3d.js实现饼状图
嘛,首先,废话一下,这个插件挺好用的.我是因为做亮灯率demo所以接触了它. 首先引用外部文件,jQuery.js,highcharts.js,highcharts-3d.js,好的,这就搞定了第一步 ...
- JMS笔记(三)
最近重看activemq,对消息的传送确认机制有了进一步认识 1. mq在确认consumer收到消息后才会删除消息,因此consumer接收消息后应该进行ack"确认",java ...
- linux awk 常见字符串处理
awk指定输出列: awk '{print $0} file' #打印所有列awk '{print $1}' file #打印第一列 awk '{print $1, $3}' file #打印第一和第 ...
- AIX 7.1 RAC 11.2.0.4.0升级至11.2.0.4.6(一个patch跑了3个小时)
1.环境 DB:两节点RAC 11.2.0.4.0升级至11.2.0.4.6 OS:AIX 7.1(205G内存 16C) 2.节点1.节点2(未建库) 2.1.patch 20420937居然用了3 ...
- mysql 处理utf8mb4的问题
jdbc端的characterEncoding=utf8 无法改为utf8mb4 测试: create table utf8mb4_test (name1 varCHAR(20) CHARACTER ...
- OI养老专题01:约瑟夫问题
有M个人,其编号分别为1-M.这M个人按顺序排成一个圈.现在给定一个数N,从第一个人开始依次报数,数到N的人出列,然后又从下一个人开始又从1开始依次报数,数到N的人又出列...如此循环,直到最后一个人 ...