最近我正在处理C#中关于timeout行为的一些bug。解决方案很是有意思,因此我在这里分享给广大博友们。多线程
我要处理的是下面这些状况:测试
咱们作了一个应用程序,程序中有这么一个模块,它的功能向用户显示一个消息对话框,15秒后再自动关闭该对话框。可是,若是用户手动关闭对话框,则在timeout时咱们无需作任何处理。线程
程序中有一个漫长的执行操做。若是该操做持续5秒钟以上,那么请终止这个操做。code
咱们的的应用程序中有执行时间未知的操做。当执行时间过长时,咱们须要显示一个“进行中”弹出窗口来提示用户耐心等待。咱们没法预估此次操做会持续多久,但通常状况下会持续不到一秒。为了不弹出窗口一闪而过,咱们只想要在1秒后显示这个弹出窗口。反之,若是在1秒内操做完成,则不须要显示这个弹出窗口。orm
这些问题是类似的。在超时以后,咱们必须执行X操做,除非Y在那个时候发生。继承
为了找到解决这些问题的办法,我在试验过程当中建立了一个类:事件
public class OperationHandler { private IOperation _operation; public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { //在超时后须要调用 "_operation.DoOperation()" } public void StopOperationIfNotStartedYet() { //在超时期间须要中止"DoOperation" } }
个人操做类:get
public class MyOperation : IOperation { public void DoOperation() { Console.WriteLine("Operation started"); } } public class MyOperation : IOperation { public void DoOperation() { Console.WriteLine("Operation started"); } }
个人测试程序:string
static void Main(string[] args) { var op = new MyOperation(); var handler = new OperationHandler(op); Console.WriteLine("Starting with timeout of 5 seconds"); handler.StartWithTimeout(5 * 1000); Thread.Sleep(6 * 1000); Console.WriteLine("Starting with timeout of 5 but cancelling after 2 seconds"); handler.StartWithTimeout(5 * 1000); Thread.Sleep(2 * 1000); handler.StopOperationIfNotStartedYet(); Thread.Sleep(4 * 1000); Console.WriteLine("Finished..."); Console.ReadLine(); }
结果应该是:
it
Starting with timeout of 5 seconds Operation started Starting with timeout of 5 but cancelling after 2 seconds Finished... |
如今咱们能够开始试验了!
我最初的计划是在另外一个不一样的线程上休眠,同时用一个布尔值来标记Stop是否被调用。
public class OperationHandler { private IOperation _operation; private bool _stopCalled; public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { Task.Factory.StartNew(() => { _stopCalled = false; Thread.Sleep(timeoutMillis); if (!_stopCalled) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { _stopCalled = true; } }
针对正常的线程执行步骤,这段代码运行过程并无出现问题,可是老是感受有些别扭。仔细探究后,我发现其中有一些猫腻。首先,在超时期间,有一个线程从线程池中取出后什么都没作,显然这个线程是被浪费了。其次,若是程序中止执行了,线程会继续休眠直到超时结束,浪费了CPU时间。
可是这些并非咱们这段代码最糟糕的事情,实际上咱们的程序实还存在一个明显的bug:
若是咱们设置10秒的超时时间,开始操做后,2秒中止,而后在2秒内再次开始。
当第二次启动时,咱们的_stopCalled标志将变成false。而后,当咱们的第一个Thread.Sleep()完成时,即便咱们取消它,它也会调用DoOperation。
以后,第二个Thread.Sleep()完成,并将第二次调用DoOperation。结果致使DoOperation被调用两次,这显然不是咱们所指望的。
若是你每分钟有100次这样的超时,我将很难捕捉到这种错误。
当StopOperationIfNotStartedYet被调用时,咱们须要某种方式来取消DoOperation的调用。
若是咱们尝试使用计时器呢?
.NET中有三种不一样类型的记时器,分别是:
这三种计时器中,System.Threading.Timer足以知足咱们的需求。这里是使用Timer的代码:
public class OperationHandler { private IOperation _operation; private Timer _timer; public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { if (_timer != null) return; _timer = new Timer( state => { _operation.DoOperation(); DisposeOfTimer(); }, null, timeoutMillis, timeoutMillis); } public void StopOperationIfNotStartedYet() { DisposeOfTimer(); } private void DisposeOfTimer() { if (_timer == null) return; var temp = _timer; _timer = null; temp.Dispose(); } }
执行结果以下:
Starting with timeout of 5 seconds Operation started Starting with timeout of 5 but cancelling after 2 seconds Finished... |
如今当咱们中止操做时,定时器被丢弃,这样就避免了再次执行操做。这已经实现了咱们最初的想法,固然还有另外一种方式来处理这个问题。
ManualResetEvent/AutoResetEvent的字面意思是手动或自动重置事件。AutoResetEvent和ManualResetEvent是帮助您处理多线程通讯的类。 基本思想是一个线程能够一直等待,知道另外一个线程完成某个操做, 而后等待的线程能够“释放”并继续运行。
ManualResetEvent类和AutoResetEvent类请参阅MSDN:
ManualResetEvent类:https://msdn.microsoft.com/zh-cn/library/system.threading.manualresetevent.aspx
AutoResetEvent类:https://msdn.microsoft.com/zh-cn/library/system.threading.autoresetevent.aspx
言归正传,在本例中,直到手动重置事件信号出现,mre.WaitOne()会一直等待。 mre.Set()将标记重置事件信号。 ManualResetEvent将释放当前正在等待的全部线程。AutoResetEvent将只释放一个等待的线程,并当即变为无信号。WaitOne()也能够接受超时做为参数。 若是Set()在超时期间未被调用,则线程被释放而且WaitOne()返回False。
如下是此功能的实现代码:
public class OperationHandler { private IOperation _operation; private ManualResetEvent _mre = new ManualResetEvent(false); public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { _mre.Reset(); Task.Factory.StartNew(() => { bool wasStopped = _mre.WaitOne(timeoutMillis); if (!wasStopped) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { _mre.Set(); } }
执行结果:
Starting with timeout of 5 seconds Operation started Starting with timeout of 5 but cancelling after 2 seconds Finished... |
我我的很是倾向于这个解决方案,它比咱们使用Timer的解决方案更干净简洁。
对于咱们提出的简单功能,ManualResetEvent和Timer解决方案均可以正常工做。 如今让咱们增长点挑战性。
新的改进需求
假设咱们如今能够连续屡次调用StartWithTimeout(),而不是等待第一个超时完成后调用。
可是这里的预期行为是什么?实际上存在如下几种可能性:
可能性1能够经过Timer和ManualResetEvent能够轻松实现。 事实上,咱们已经在咱们的Timer解决方案中涉及到了这个。
public void StartWithTimeout(int timeoutMillis) { if (_timer != null) return; ... public void StartWithTimeout(int timeoutMillis) { if (_timer != null) return; ... }
可能性2也能够很容易地实现。 这个地方请容许我卖个萌,代码本身写哈^_^
可能性3不可能经过使用Timer来实现。 咱们将须要有一个定时器的集合。 一旦中止操做,咱们须要检查并处理定时器集合中的全部子项。 这种方法是可行的,但经过ManualResetEvent咱们能够很是简洁和轻松的实现这一点!
可能性4跟可能性3类似,能够经过定时器的集合来实现。
可能性3:使用单个ManualResetEvent中止全部操做
让咱们了解一下这里面遇到的难点:
假设咱们调用StartWithTimeout 10秒超时。
1秒后,咱们再次调用另外一个StartWithTimeout,超时时间为10秒。
再过1秒后,咱们再次调用另外一个StartWithTimeout,超时时间为10秒。
预期的行为是这3个操做会依次10秒、11秒和12秒后启动。
若是5秒后咱们会调用Stop(),那么预期的行为就是全部正在等待的操做都会中止, 后续的操做也没法进行。
我稍微改变下Program.cs,以便可以测试这个操做过程。 这是新的代码:
class Program { static void Main(string[] args) { var op = new MyOperation(); var handler = new OperationHandler(op); Console.WriteLine("Starting with timeout of 10 seconds, 3 times"); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(13 * 1000); Console.WriteLine("Starting with timeout of 10 seconds 3 times, but cancelling after 5 seconds"); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(5 * 1000); handler.StopOperationIfNotStartedYet(); Thread.Sleep(8 * 1000); Console.WriteLine("Finished..."); Console.ReadLine(); } }
下面就是使用ManualResetEvent的解决方案:
public class OperationHandler { private IOperation _operation; private ManualResetEvent _mre = new ManualResetEvent(false); public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { Task.Factory.StartNew(() => { bool wasStopped = _mre.WaitOne(timeoutMillis); if (!wasStopped) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { Task.Factory.StartNew(() => { _mre.Set(); Thread.Sleep(10);//This is necessary because if calling Reset() immediately, not all waiting threads will 'proceed' _mre.Reset(); }); } }
输出结果跟预想的同样:
Starting with timeout of 10 seconds, 3 times Operation started Operation started Operation started Starting with timeout of 10 seconds 3 times, but cancelling after 5 seconds Finished... |
很开森对不对?
当我检查这段代码时,我发现Thread.Sleep(10)是必不可少的,这显然超出了个人意料。 若是没有它,除3个等待中的线程以外,只有1-2个线程正在进行。 很明显的是,由于Reset()发生得太快,第三个线程将停留在WaitOne()上。
可能性4:单个AutoResetEvent中止一个随机操做
假设咱们调用StartWithTimeout 10秒超时。1秒后,咱们再次调用另外一个StartWithTimeout,超时时间为10秒。再过1秒后,咱们再次调用另外一个StartWithTimeout,超时时间为10秒。而后咱们调用StopOperationIfNotStartedYet()。
目前有3个操做超时,等待启动。 预期的行为是其中一个被中止, 其余2个操做应该可以正常启动。
咱们的Program.cs能够像之前同样保持不变。 OperationHandler作了一些调整:
public class OperationHandler { private IOperation _operation; private AutoResetEvent _are = new AutoResetEvent(false); public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { _are.Reset(); Task.Factory.StartNew(() => { bool wasStopped = _are.WaitOne(timeoutMillis); if (!wasStopped) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { _are.Set(); } }
执行结果是:
Starting with timeout of 10 seconds, 3 times Operation started Operation started Operation started Starting with timeout of 10 seconds 3 times, but cancelling after 5 seconds Operation started Operation started Finished... |
在处理线程通讯时,超时后继续执行某些操做是常见的应用。咱们尝试了一些很好的解决方案。一些解决方案可能看起来不错,甚至能够在特定的流程下工做,可是也有可能在代码中隐藏着致命的bug。当这种状况发生时,咱们应对时须要特别当心。
AutoResetEvent和ManualResetEvent是很是强大的类,我在处理线程通讯时一直使用它们。这两个类很是实用。正在跟线程通讯打交道的朋友们,快把它们加入到项目里面吧!