最近我正在处理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(),而不是等待第一个超时完成后调用。

但是这里的预期行为是什么?实际上存在以下几种可能性:

  1. 在以前的StartWithTimeout超时期间调用StartWithTimeout时:忽略第二次启动。
  2. 在以前的StartWithTimeout超时期间调用StartWithTimeout时:停止初始话Start并使用新的StartWithTimeout。
  3. 在以前的StartWithTimeout超时期间调用StartWithTimeout时:在两个启动中调用DoOperation。 在StopOperationIfNotStartedYet中停止所有尚未开始的操作(在超时时间内)。
  4. 在以前的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是非常强大的类,我在处理线程通信时一直使用它们。这两个类非常实用。正在跟线程通信打交道的朋友们,快把它们加入到项目里面吧!

转载于:https://www.cnblogs.com/yayazi/p/8328468.html

C#中的多线程超时处理实践相关推荐

  1. delphi多线程超时控Delphi7中Indy控件对于网络数据的接收

    1.引言 随着我国经济和社会的发展,水资源的科学管理与合理配置显得越来越重要.而获取大量的.实时的.动态的水资源及其相关信息则是实现水资源科学管理的基础.传统的水资源信息获取采用人工抄取数据后逐级的方 ...

  2. Java中的多线程编程(超详细总结)

    文章目录 Java中的多线程编程(超详细总结) 一.线程与多线程的概念 二.线程与进程之间的关系 三.一个线程的生命周期 四.多线程的目的和意义 五.线程的实现的方式 Java中的多线程编程(超详细总 ...

  3. 一起谈.NET技术,在.NET Workflow 3.5中使用多线程提高工作流性能

    最近在工作上碰到一个性能问题,由于项目是基于SOA的架构,使得整个系统完全依赖于各种各样的Service,其中用于处理业务逻辑的Business Services全部都用.NET Workflow 3 ...

  4. C#中的多线程 - 同步基础

    C#中的多线程 - 同步基础 C#中的多线程 - 同步基础 1同步概要 在第 1 部分:基础知识中,我们描述了如何在线程上启动任务.配置线程以及双向传递数据.同时也说明了局部变量对于线程来说是私有的, ...

  5. C#中的多线程 - 并行编程 z

    原文:http://www.albahari.com/threading/part5.aspx 专题:C#中的多线程 1并行编程Permalink 在这一部分,我们讨论 Framework 4.0 加 ...

  6. java怎样获取线程的进度_java中的多线程——进度2

    多线程总结: 1,进程和线程的概念. |--进程:是一块包含了某些资源的内存区域.操作系统利用进程把它的工作划分为一些功能单元: 最小的内存单元: 是具有一定独立功能的程序关于某个数据集合上的一次运行 ...

  7. Java多线程超时判断

    Java多线程超时判断 应用场景 主要方法 实现代码 应用场景 多线程任务中,个别线程可能发生阻塞,无法正常返回,如果等待全部线程执行完毕,程序将无法正常执行结束.此时需要为多线程设置最大执行时间,超 ...

  8. Android中的多线程编程与异步处理

    Android中的多线程编程与异步处理 引言 在移动应用开发中,用户体验是至关重要的.一个流畅.高效的应用能够吸引用户并提升用户满意度.然而,移动应用面临着处理复杂业务逻辑.响应用户输入.处理网络请求 ...

  9. python如何在网络爬虫程序中使用多线程(threading.Thread)

    python如何在网络爬虫程序中使用多线程 一.多线程的基础知识 二.在网络爬虫中使用多线程 2.1 从单线程版本入手 2.2 将单线程版本改写为多线程版本 2.3 运行多线程版本程序 2.4 将多线 ...

最新文章

  1. 人类语言的表现形式和规则
  2. .NET WinForm中给DataGridView自定义ToolTip并设置ToolTip的样式
  3. html外链式css运行不出来div,html+css外链式
  4. SpringBoot微服务 b2b2c 多用户商城系统(八)springboot整合mongodb
  5. stm32怎么加载字库_收藏 | STM32单片机超详细学习汇总资料(二)
  6. 在cmd指令看计算机位数,在.cmd中使用Windows命令来测试32位或64位并运行命令
  7. android--调用系统浏览器,Android 调用系统浏览器
  8. 架构畅想:如果以你所会去进行架构,会到哪一步?
  9. 企业局域网内如何跨网安全传输数据
  10. linux行位换行符,换行符或标点符号作为elasticsearch中的位置间隔
  11. 全球互联网大面积瘫痪不再是虚幻
  12. spring cloud-熔断(六)
  13. 从小米雷军的逆天布局你能读出什么?
  14. 关于对皮亚诺公理的理解
  15. JavaScript异步与同步解析
  16. redefinition; different type modifiers错误解决
  17. AppsFlyer SDK 接入
  18. adb shell am 命令(活动管理)
  19. 【解决】win10下emqx启动报错Unable to load emulator DLL、node.db_role = EMQX_NODE__DB_ROLE = core
  20. Apache Beam

热门文章

  1. 2017战略No.1:坚定不移地走全产业链发展路线
  2. 小青柑的功效与作用,这些你都知道吗?
  3. 1天烧掉10万美元的ChatGPT正式开放API:成本大砍90%,75万个单词仅收费2美元
  4. 【华为OD统一考试B卷 | 200分】 学生方阵(C++ Java JavaScript Python)
  5. 一文搞懂 Prometheus 多集群监控神器 Thanos
  6. Python3实现百度贴吧帖子搜索
  7. ProPlusWW.msi
  8. 燃气表全国产化电子元件推荐方案
  9. 【计组】Cache 全相连 组相联 直联
  10. cad线性标注命令_CAD线性标注快捷键DLI,CAD标注快捷键大全