在情景一、情景二中,我分别介绍了当多线程遇到 “资源争用”、“限量使用” 情形时的解决方案,本篇是本系列的最后一种情形,会介绍几种用于解决线程通信的方案。
情景三:我让你动,你才能动!
大锤:“老板,拿这个手机让我看看”。
大锤:“这是手机吗??? 分别就只是一个壳子”。
老板:“呀,这可能是生产上出了问题,我给你换一个!”
大锤:“老板,你这是当我是傻子呢?还是傻子呢?还是傻子呢? 这回给我的手机怎么没有电源啊!我要怎么开机啊!”
万万没想到,经过千挑万选,最终还是找到了一个配件完整的手机。
老板回去后发现了原因是:生产和上线销售两个环节没有搭配好,当生产的环节还没有结束时,就把中间产物拿去销售了。
解决办法:所有动作不能擅自执行,必须服从命令,当生产环节完成时会通知上线环节,然后才被允许拿到市场上去销售。
问题抽象:当某个操作的执行必须依赖于另一个操作的完成时,需要有个机制来保证这种先后关系。
线程通信方案:ManualResetEventSlim、ManualResetEvent、AutoResetEvent
方案特性:提供线程通知的能力,没有接到通知前,线程必须等待,有先后顺序。
各方案间的区别
在继续阅读前,请确保你已经对用户模式构造、内核模式构造和混合模式构造有所了解,如果尚未了解,建议您先阅读情景一中相关章节。
内核模式(kernal-mode)
ManualResetEvent 和 AutoResetEvent 都继承自 EventWaitHandle 并最终与 Mutex 和 Semaphore 一样拥有共同的祖宗: WaitHandle。在前面几篇中我有讲过 WaitHandle 是一个抽象类,包装了 Windows 操作系统的内核对象句柄。
ManualResetEvent: 中文理解就是手动重置事件(这里的事件并不是我们通常意义中按钮的那种事件,更多的应该理解为一个通告)。所有事件的初始状态都为 “不可用”,任何在等待该事件的线程都将一直等待下去。只有当通过 Set 方法释放了一个信号后,等待该事件的线程才被允许执行所需的操作。如果不手动重置,那么状态一直为 “可用”,任何等待该事件的线程可以继续执行操作。只有当调用 Reset 方法后,状态才会变为 “不可用”。通过 WaitOne 来请求状态,从而决定是否执行操作。
这个特点,有点类似红绿灯,当绿灯亮起,所有机动车都被允许通过,直到再次变成红灯。
与 Mutex 不同的是,Reset 和 Set 操作可以由不同的线程发起。如:
ManualResetEvent s = new ManualResetEvent(false); Task.Factory.StartNew(() => { s.Set(); }); Task.Factory.StartNew(() => { s.Reset(); }); Task.Factory.StartNew(() => { s.WaitOne(); });
优点:提供线程间通信的能力,可以跨进程使用。
缺点:速度慢于用户模式、混合模式构造,稍快于 mutex。
AutoResetEvent: 顾名思义,就是自动重置事件。如果 ManulResetEvent 相当于红绿灯,那 AutoResetEvent 就类似高速入口的闸机,杆抬起一次,通过一辆车。第二辆车要重新等待杆抬起。当调用 Set 后,状态变成 “可用”,但只要一执行 WaitOne 请求状态后,状态即可变成 “不可用”(这个过程不需要 Reset 方法的参与)。
优点:提供线程间通信的能力,可以跨进程使用。
缺点:速度慢于用户模式、混合模式构造,稍快于 mutex。
混合模式(hybrid-mode)
在 .Net 4.0 时候引入了 ManualResetEventSlim 来提高性能。下面是 MSDN 的原话:
在 .NET Framework 4 版中,当等待时间预计非常短时,并且当事件不会跨越进程边界时,可使用 System.Threading.ManualResetEventSlim 类以获得更好的性能。当等待事件变为已发出信号状态的过程中,ManualResetEventSlim 短时间内会使用繁忙旋转。 当等待时间很短时,旋转的开销相对于使用等待句柄来进行等待的开销会少很多。 但是,如果事件在某个时间段内没有变为已发出信号状态,则 ManualResetEventSlim 会采用常规的事件处理等待。
ManualResetEventSlim 的用法与 ManualResetEvent 几乎相似,只是原先使用 WaitOne 的地方需要使用 Wait 代替。
优点:提供线程间通信的能力。
缺点:不能跨进程使用,速度快于内核模式构造。
总 结
本篇文章所解决的是当两个或多个线程之间需要按某种顺序执行的时候,线程间的同步问题。如果在开发中遇到两个线程需要按某种顺序先后执行的,则应该考虑使用 ManualResetEvent 或 AutoResetEvent。
本文来自《C# 基础回顾: 线程同步的情景之三》