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(); } }
执行结果如下:
starting with timeout of 5 seconds operation started starting with timeout of 5 but cancelling after 2 seconds finished...
现在当我们停止操作时,定时器被丢弃,这样就避免了再次执行操作。这已经实现了我们最初的想法,当然还有另一种方式来处理这个问题。
解决方案3:manualresetevent或autoresetevent
manualresetevent/autoresetevent的字面意思是手动或自动重置事件。autoresetevent和manualresetevent是帮助您处理多线程通信的类。 基本思想是一个线程可以一直等待,知道另一个线程完成某个操作, 然后等待的线程可以“释放”并继续运行。
manualresetevent类和autoresetevent类请参阅msdn:
manualresetevent类:
autoresetevent类:
言归正传,在本例中,直到手动重置事件信号出现,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(),而不是等待第一个超时完成后调用。
但是这里的预期行为是什么?实际上存在以下几种可能性:
- 在以前的startwithtimeout超时期间调用startwithtimeout时:忽略第二次启动。
- 在以前的startwithtimeout超时期间调用startwithtimeout时:停止初始话start并使用新的startwithtimeout。
- 在以前的startwithtimeout超时期间调用startwithtimeout时:在两个启动中调用dooperation。 在stopoperationifnotstartedyet中停止所有尚未开始的操作(在超时时间内)。
- 在以前的startwithtimeout超时期间调用startwithtimeout时:在两个启动中调用dooperation。 在stopoperationifnotstartedyet停止一个尚未开始的随机操作。
可能性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是非常强大的类,我在处理线程通信时一直使用它们。这两个类非常实用。正在跟线程通信打交道的朋友们,快把它们加入到项目里面吧!
总结
以上所述是小编给大家介绍的c#中的多线程超时处理实践方案,希望对大家有所帮助