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类: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(),而不是等待第一个超时完成后调用。
但是这里的预期行为是什么?实际上存在以下几种可能性:
- 在以前的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#中的多线程超时处理实践的更多相关文章
- java中异步多线程超时导致的服务异常
在项目中为了提高大并发量时的性能稳定性,经常会使用到线程池来做多线程异步操作,多线程有2种,一种是实现runnable接口,这种没有返回值,一种是实现Callable接口,这种有返回值. 当其中一个线 ...
- python threading模块使用 以及python多线程操作的实践(使用Queue队列模块)
今天花了近乎一天的时间研究python关于多线程的问题,查看了大量源码 自己也实践了一个生产消费者模型,所以把一天的收获总结一下. 由于GIL(Global Interpreter Lock)锁的关系 ...
- C#中的多线程 - 高级多线程 z
原文:http://www.albahari.com/threading/part4.aspx 专题:C#中的多线程 1非阻塞同步Permalink 之前,我们描述了即使是很简单的赋值或更新一个字段也 ...
- C#中的多线程 - 基础知识 z
原文:http://www.albahari.com/threading/ 专题:C#中的多线程 1简介及概念Permalink C# 支持通过多线程并行执行代码,线程有其独立的执行路径,能够与其它线 ...
- Java中的多线程=你只要看这一篇就够了
如果对什么是线程.什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内. 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现.说这个话其 ...
- 结合异步模型,再次总结Netty多线程编码最佳实践
更多技术分享可关注我 前言 本文重点总结Netty多线程的一些编码最佳实践和注意事项,并且顺便对Netty的线程调度模型,和异步模型做了一个汇总.原文:结合异步模型,再次总结Netty多线程编码最 ...
- python中的多线程【转】
转载自: http://c4fun.cn/blog/2014/05/06/python-threading/ python中关于多线程的操作可以使用thread和threading模块来实现,其中th ...
- 【转】Java中的多线程学习大总结
多线程作为Java中很重要的一个知识点,在此还是有必要总结一下的. 一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: 上图中基本上囊括了Java中多线程 ...
- C#中的多线程 - 并行编程 z
原文:http://www.albahari.com/threading/part5.aspx 专题:C#中的多线程 1并行编程Permalink 在这一部分,我们讨论 Framework 4.0 加 ...
随机推荐
- 如何在MQ中实现支持任意延迟的消息?
什么是定时消息和延迟消息? 定时消息:Producer 将消息发送到 MQ 服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消 ...
- ES6 Proxy和Reflect (上)
Proxy概述 Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种"元编程"(meta programming),即对编程语言进行编程. Proxy可以理 ...
- 使用Linux 安装MySQL
文章 link 在安装mysql数据库服务器前,确保你的linux系统是可以连接网络的,下面我们将通过源码方式来安装mysql首先通过putty登入进你的Linux系统,确保系统中已经安装的gcc ...
- MVC 框架
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑.数据.界面显示分离的方法组织代码 ...
- Android 7.1 WindowManagerService 屏幕旋转流程分析 (三)
三.屏幕的绘制 performSurfacePlacement()函数来触发window的绘制,这里最大的循环次数是6,当然一般不会到最大次数就会被Scheduled. final void perf ...
- QA问答系统,QA匹配论文学习笔记
论文题目: WIKIQA: A Challenge Dataset for Open-Domain Question Answering 论文代码运行: 首先按照readme中的提示安装需要的部分 遇 ...
- Git详解之三:Git分支
Git 分支 几乎每一种版本控制系统都以某种形式支持分支.使用分支意味着你可以从开发主线上分离开来,然后在不影响主线的同时继续工作.在很多版本控制系统中,这是个昂贵的过程,常常需要创建一个源代码目录的 ...
- C# 防止同时调用=========使用读写锁三行代码简单解决多线程并发的问题
http://www.jb51.net/article/99718.htm 本文主要介绍了C#使用读写锁三行代码简单解决多线程并发写入文件时提示"文件正在由另一进程使用,因此该进程无 ...
- 前端学习_01_css网页布局
引子 之前也自己陆陆续续地学了一些web方面的知识,包括前段和后端都有涉及到,自己也比较感兴趣,感谢peter老师,愿意无偿提供从零开始的教学,之前也看过peter老师的一些视频,节奏非常适合我,决心 ...
- jQuery 实现无限任意添加下拉菜单
新学jQuery还有很多没学,今天做了个下拉菜单,按照自己的思想结合学的基础效果实现一款可以任意添加层数的下拉菜单,如果有什么建议,欢迎指教啦啦啦 我喜欢备注细一些,这样给自己也是一种理解和方便回顾哈 ...