C#中的多线程超时处理实践

最近我正在处理C#中关于timeout行为的一些bug。解决方案非常有意思,所以我在这里分享给广大博友们。

我要处理的是下面这些情况:

我们做了一个应用程序,程序中有这么一个模块,它的功能向用户显示一个消息对话框,15秒后再自动关闭该对话框。但是,如果用户手动关闭对话框,则在timeout时我们无需做任何处理。

程序中有一个漫长的执行操作。如果该操作持续5秒钟以上,那么请终止这个操作。

我们的的应用程序中有执行时间未知的操作。当执行时间过长时,我们需要显示一个“进行中”弹出窗口来提示用户耐心等待。我们无法预估这次操作会持续多久,但一般情况下会持续不到一秒。为了避免弹出窗口一闪而过,我们只想要在1秒后显示这个弹出窗口。反之,如果在1秒内操作完成,则不需要显示这个弹出窗口。

这些问题是相似的。在超时之后,我们必须执行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" } }

我的操作类:

public class MyOperation : IOperation { public void DoOperation() { Console.WriteLine("Operation started"); } } public class MyOperation : IOperation { public void DoOperation() { Console.WriteLine("Operation started"); } }

我的测试程序:

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(); }

结果应该是:


Starting with timeout of 5 seconds
Operation started
Starting with timeout of 5 but cancelling after 2 seconds
Finished...
 

现在我们可以开始试验了!

解决方案1:在另一个线程上休眠

我最初的计划是在另一个不同的线程上休眠,同时用一个布尔值来标记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的调用。

如果我们尝试使用计时器呢?

解决方案2:使用计时器

.NET中有三种不同类型的记时器,分别是:

System.Windows.Forms命名空间下的Timer控件,它直接继承自Componet。

System.Timers命名空间下的Timer类。

System.Threading.Timer类。

这三种计时器中,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(); } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zysfxz.html