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

发表于:2018-3-06 09:29

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:阿子    来源:博客园

#
DoNet
分享:
  最近我正在处理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类: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解决方案都可以正常工作。 现在让我们增加点挑战性。
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号