System.Threading.Timer是.NET中一个定时触发事件处理方法的类(本文后面简称Timer),它背后依靠的是.NET的线程池(ThreadPool),所以当Timer在短时间内触发了过多的事件处理方法后,可能会造成事件处理方法在线程池(ThreadPool)中排队,可以参考这篇文章

我们启动Timer后,如果我们想停止它,必须要用到Timer.Dispose方法,该方法会让Timer停止启动新的线程去执行事件处理方法,但是已经在线程池(ThreadPool)中处理和排队的事件处理方法还是会被继续执行,而Timer.Dispose方法会立即返回,它并不会被阻塞来等待剩下在线程池(ThreadPool)中处理和排队的事件处理方法都执行完毕。

所以这个时候我们需要一个机制来知道当Timer.Dispose方法被调用后,剩下在线程池(ThreadPool)中处理和排队的事件处理方法,是否都已经被执行完毕了。这个时候我们需要用到Timer的bool Dispose(WaitHandle notifyObject)重载方法,这个Dispose方法会传入一个WaitHandle notifyObject参数,当Timer剩下在线程池(ThreadPool)中处理和排队的事件处理方法都执行完毕后,Timer会给Dispose方法传入的WaitHandle notifyObject参数发出一个信号,而我们可以通过WaitHandle.WaitOne方法来等待该信号,在收到信号前WaitHandle.WaitOne方法会被一直阻塞,代码如下所示(基于.NET Core控制台项目):

using System;
using System.Threading; namespace TimerDispose
{
class Program
{
static Timer timer = null;
static ManualResetEvent timerDisposed = null;//ManualResetEvent继承WaitHandle
static int timeCount = ; static void CreateAndStartTimer()
{
//初始化Timer,设置触发间隔为2000毫秒,设置dueTime参数为Timeout.Infinite表示不启动Timer
timer = new Timer(TimerCallBack, null, Timeout.Infinite, );
//启动Timer,设置dueTime参数为0表示立刻启动Timer
timer.Change(, );
} /// <summary>
/// TimerCallBack方法是Timer每一次触发后的事件处理方法
/// </summary>
static void TimerCallBack(object state)
{
//模拟做一些处理逻辑的事情 timeCount++;//每一次Timer触发调用TimerCallBack方法后,timeCount会加1 //当timeCount为100的时候,调用Timer.Change方法来改变Timer的触发间隔为1000毫秒
if (timeCount == )
{
timer.Change(, );
}
} static void Main(string[] args)
{
CreateAndStartTimer(); Console.WriteLine("按任意键调用Timer.Dispose方法...");
Console.ReadKey(); timerDisposed = new ManualResetEvent(false);
timer.Dispose(timerDisposed);//调用Timer的bool Dispose(WaitHandle notifyObject)重载方法,来结束Timer的触发,当线程池中的所有TimerCallBack方法都执行完毕后,Timer会发一个信号给timerDisposed timerDisposed.WaitOne();//WaitHandle.WaitOne()方法会等待收到一个信号,否则一直被阻塞
timerDisposed.Dispose(); Console.WriteLine("Timer已经结束,按任意键结束整个程序...");
Console.ReadKey();
}
}
}

但是我们上面的代码中的TimerCallBack事件处理方法有一个逻辑,也就是当timeCount变量增加到100的时候,我们会调用Timer.Change方法,更改Timer的触发间隔为1000毫秒。而Timer.Change方法是不能够在Timer.Dispose方法后调用的,也就是说当一个Timer调用了Dispose方法后,就不能再调用Timer.Change方法了,否则Timer.Change方法会抛出ObjectDisposedException异常,对此MSDN上的解释如下:

If the callback uses the Change method to set the dueTime parameter to zero, a race condition can occur when the Dispose(WaitHandle) method overload is called: If the timer queues a new callback before the Dispose(WaitHandle) method overload detects that there are no callbacks queued, Dispose(WaitHandle) continues to block; otherwise, the timer is disposed while the new callback is being queued, and an ObjectDisposedException is thrown when the new callback calls the Change method.

然而在我们的代码中调用Timer.Dispose方法和TimerCallBack事件处理方法是并行的,因为Timer.Dispose方法是在程序主线程上执行的,而TimerCallBack事件处理方法是在线程池(ThreadPool)中的线程上执行的,所以Timer.Dispose方法执行后,很有可能会再执行TimerCallBack事件处理方法,这时候如果恰好timeCount变量也增加到100了,会导致Timer.Change方法在Timer.Dispose方法后执行,抛出ObjectDisposedException异常。

对此我们要对我们的代码稍作更改,在TimerCallBack事件处理方法中来捕捉ObjectDisposedException异常:

using System;
using System.Threading; namespace TimerDispose
{
class Program
{
static Timer timer = null;
static ManualResetEvent timerDisposed = null;//ManualResetEvent继承WaitHandle
static int timeCount = ; static void CreateAndStartTimer()
{
//初始化Timer,设置触发间隔为2000毫秒,设置dueTime参数为Timeout.Infinite表示不启动Timer
timer = new Timer(TimerCallBack, null, Timeout.Infinite, );
//启动Timer,设置dueTime参数为0表示立刻启动Timer
timer.Change(, );
} /// <summary>
/// TimerCallBack方法是Timer每一次触发后的事件处理方法
/// </summary>
static void TimerCallBack(object state)
{
//模拟做一些处理逻辑的事情 timeCount++;//每一次Timer触发调用TimerCallBack方法后,timeCount会加1 //当timeCount为100的时候,调用Timer.Change方法来改变Timer的触发间隔为1000毫秒
if (timeCount == )
{
//添加try catch代码块,来捕捉Timer.Change方法抛出的ObjectDisposedException异常
try
{
timer.Change(, );
}
catch (ObjectDisposedException)
{
//当Timer.Change方法抛出ObjectDisposedException异常后的处理逻辑
Console.WriteLine("在Timer.Dispose方法执行后,再调用Timer.Change方法已经没有意义");
}
}
} static void Main(string[] args)
{
CreateAndStartTimer(); Console.WriteLine("按任意键调用Timer.Dispose方法...");
Console.ReadKey(); timerDisposed = new ManualResetEvent(false);
timer.Dispose(timerDisposed);//调用Timer的bool Dispose(WaitHandle notifyObject)重载方法,来结束Timer的触发,当线程池中的所有TimerCallBack方法都执行完毕后,Timer会发一个信号给timerDisposed timerDisposed.WaitOne();//WaitHandle.WaitOne()方法会等待收到一个信号,否则一直被阻塞
timerDisposed.Dispose(); Console.WriteLine("Timer已经结束,按任意键结束整个程序...");
Console.ReadKey();
}
}
}

所以这样我们可以防止Timer.Change方法在Timer.Dispose方法后意外抛出ObjectDisposedException异常使整个程序报错终止,至少异常抛出时我们是有代码去处理的。

而国外的一位高手不仅考虑到了Timer.Change方法会抛出ObjectDisposedException异常,他还给WaitHandle.WaitOne方法添加了超时限制(_disposalTimeout),并且还加入了逻辑来防止Timer.Dispose方法被多次重复调用,注意Timer的bool Dispose(WaitHandle notifyObject)重载方法是会返回一个bool值的,如果它返回了false,那么表示Timer.Dispose方法已经被调用过了,代码如下所示:

using System;
using System.Threading; namespace TimerDispose
{
class SafeTimer
{
private readonly TimeSpan _disposalTimeout; private readonly System.Threading.Timer _timer; private bool _disposeEnded; public SafeTimer(TimeSpan disposalTimeout)
{
_disposalTimeout = disposalTimeout;
_timer = new System.Threading.Timer(HandleTimerElapsed);
} public void TriggerOnceIn(TimeSpan time)
{
try
{
_timer.Change(time, Timeout.InfiniteTimeSpan);
}
catch (ObjectDisposedException)
{
// race condition with Dispose can cause trigger to be called when underlying
// timer is being disposed - and a change will fail in this case.
// see
// https://msdn.microsoft.com/en-us/library/b97tkt95(v=vs.110).aspx#Anchor_2
if (_disposeEnded)
{
// we still want to throw the exception in case someone really tries
// to change the timer after disposal has finished
// of course there's a slight race condition here where we might not
// throw even though disposal is already done.
// since the offending code would most likely already be "failing"
// unreliably i personally can live with increasing the
// "unreliable failure" time-window slightly
throw;
}
}
} //Timer每一次触发后的事件处理方法
private void HandleTimerElapsed(object state)
{
//Do something
} public void Dispose()
{
using (var waitHandle = new ManualResetEvent(false))
{
// returns false on second dispose
if (_timer.Dispose(waitHandle))
{
if (!waitHandle.WaitOne(_disposalTimeout))
{
throw new TimeoutException(
"Timeout waiting for timer to stop. (...)");
}
_disposeEnded = true;
}
}
}
}
}

可以参考这个链接查看详情,需要注意的是里面有说到几点:

第1点:

Timer.Dispose(WaitHandle) can return false. It does so in case it's already been disposed (i had to look at the source code). In that case it won't set the WaitHandle - so don't wait on it! (Note: multiple disposal should be supported)

也就是说如果Timer的bool Dispose(WaitHandle notifyObject)重载方法返回了false,Timer是不会给WaitHandle notifyObject参数发出信号的,所以当Dispose方法返回false时,不要去调用WaitHandle.WaitOne方法。

第2点:

Timer.Dispose(WaitHandle) does not work properly with -Slim waithandles, or not as one would expect. For example, the following does not work (it blocks forever):
 
using (var manualResetEventSlim = new ManualResetEventSlim())
{
timer.Dispose(manualResetEventSlim.WaitHandle);
manualResetEventSlim.Wait();
}

也就是说不要用ManualResetEventSlim,否则ManualResetEventSlim.Wait方法会一直阻塞下去。

.NET的垃圾回收机制GC会回收销毁System.Threading.Timer

有一点需要注意,一旦我们创建并启动一个Timer对象后,它就自己在那里运行了,如果我们没有变量引用创建的Timer对象,那么.NET的垃圾回收机制GC会随时销毁我们创建的Timer对象,例如下面代码:

using System;
using System.Threading; namespace TimerDispose
{
class Program
{
static void CreateAndStartTimer()
{
//初始化并启动Timer,设置触发间隔为2000毫秒,设置dueTime参数为0表示立刻启动Timer
//由于我们这里创建的Timer对象没有被任何变量引用,只存在于方法CreateAndStartTimer中,所以.NET的垃圾回收机制GC会随时销毁该Timer对象
new Timer(TimerCallBack, null, , );
} /// <summary>
/// TimerCallBack方法是Timer每一次触发后的事件处理方法
/// </summary>
static void TimerCallBack(object state)
{
//模拟做一些处理逻辑的事情
} static void Main(string[] args)
{
CreateAndStartTimer(); Console.WriteLine("按任意键结束整个程序...");
Console.ReadKey();
}
}
}

上面代码的问题在于我们在CreateAndStartTimer方法中创建的Timer对象,没有被任何外部变量引用,只存在于CreateAndStartTimer方法中,所以一旦CreateAndStartTimer方法执行完毕后,Timer对象随时可能会被.NET的垃圾回收机制GC销毁,而这可能并不是我们期望的行为。

对此有如下解决方案:

在CreateAndStartTimer方法中创建Timer对象后,将其指定给一个程序全局都可以访问到的变量:

using System;
using System.Threading; namespace TimerDispose
{
class Program
{
//变量timer,用于引用CreateAndStartTimer方法内部创建的Timer对象
static Timer timer = null; static void CreateAndStartTimer()
{
//初始化Timer,设置触发间隔为2000毫秒,设置dueTime参数为Timeout.Infinite表示不启动Timer
//将创建的Timer对象,指定给一个程序全局都可以访问到的变量timer,防止Timer对象被.NET的垃圾回收机制GC销毁
timer = new Timer(TimerCallBack, null, Timeout.Infinite, );
//启动Timer,设置dueTime参数为0表示立刻启动Timer
timer.Change(, );
} /// <summary>
/// TimerCallBack方法是Timer每一次触发后的事件处理方法
/// </summary>
static void TimerCallBack(object state)
{
//模拟做一些处理逻辑的事情
} static void Main(string[] args)
{
CreateAndStartTimer(); Console.WriteLine("按任意键结束整个程序...");
Console.ReadKey(); ManualResetEvent timerDisposed = new ManualResetEvent(false);
timer.Dispose(timerDisposed); timerDisposed.WaitOne();
timerDisposed.Dispose();
}
}
}

由于现在CreateAndStartTimer方法内部创建的Timer对象,可以通过变量timer被整个程序访问到,所以就不会被.NET的垃圾回收机制GC销毁掉了。

为什么要先初始化Timer,再启动Timer

上面的代码中可以看到,我们都是先初始化Timer,再启动Timer,如下所示:

//初始化Timer,设置触发间隔为2000毫秒,设置dueTime参数为Timeout.Infinite表示不启动Timer
timer = new Timer(TimerCallBack, null, Timeout.Infinite, );
//启动Timer,设置dueTime参数为0表示立刻启动Timer
timer.Change(, );

那么我们为什么不将初始化和启动Timer在一行代码中完成呢,如下所示:

//初始化并启动Timer,设置触发间隔为2000毫秒,设置dueTime参数为0表示立刻启动Timer
timer = new Timer(TimerCallBack, null, , );

要解释这个问题,我们先来看看下面的代码:

using System;
using System.Threading; namespace TimerDispose
{
class Program
{
static Timer timer = null; static void CreateAndStartTimer()
{
//初始化并启动Timer,设置触发间隔为2000毫秒,设置dueTime参数为0表示立刻启动Timer
timer = new Timer(TimerCallBack, null, , );
} /// <summary>
/// TimerCallBack方法是Timer每一次触发后的事件处理方法
/// </summary>
static void TimerCallBack(object state)
{
//模拟做一些处理逻辑的事情 //调用Timer.Change方法来改变Timer的触发间隔为1000毫秒
//由于调用下面timer.Change方法时,可能CreateAndStartTimer方法中的timer变量还没有被赋值,timer变量为null,所以会引发NullReferenceException异常
timer.Change(, );
} static void Main(string[] args)
{
CreateAndStartTimer(); Console.WriteLine("按任意键结束整个程序...");
Console.ReadKey(); ManualResetEvent timerDisposed = new ManualResetEvent(false);
timer.Dispose(timerDisposed); timerDisposed.WaitOne();
timerDisposed.Dispose();
}
}
}

前面我们说了,TimerCallBack事件处理方法是在线程池(ThreadPool)中的线程上执行的,而我们可以看到CreateAndStartTimer方法是在程序主线程上执行的,所以当我们在CreateAndStartTimer方法中,立刻启动Timer后,TimerCallBack事件处理方法就开始在线程池(ThreadPool)中的线程上执行了,而这时有可能在CreateAndStartTimer方法中主线程还没执行到给timer变量赋值这个步骤(new Timer(...)执行完了,但是还没来得及给左边的变量timer赋值),所以会导致TimerCallBack事件处理方法中执行timer.Change时,timer变量还为null,引发NullReferenceException异常。

因此我们必须要保证当timer变量被赋值了后,才启动Timer对象,如下所示,先初始化Timer并赋值给timer变量,再启动Timer:

using System;
using System.Threading; namespace TimerDispose
{
class Program
{
static Timer timer = null; static void CreateAndStartTimer()
{
//初始化Timer,设置触发间隔为2000毫秒,设置dueTime参数为Timeout.Infinite表示不启动Timer
timer = new Timer(TimerCallBack, null, Timeout.Infinite, );
//启动Timer,设置dueTime参数为0表示立刻启动Timer,此时timer变量肯定不会为null了
timer.Change(, );
} /// <summary>
/// TimerCallBack方法是Timer每一次触发后的事件处理方法
/// </summary>
static void TimerCallBack(object state)
{
//模拟做一些处理逻辑的事情 //调用Timer.Change方法来改变Timer的触发间隔为1000毫秒
//由于调用下面timer.Change方法时,timer变量已经被赋值不为null,所以不会引发NullReferenceException异常
timer.Change(, );
} static void Main(string[] args)
{
CreateAndStartTimer(); Console.WriteLine("按任意键结束整个程序...");
Console.ReadKey(); ManualResetEvent timerDisposed = new ManualResetEvent(false);
timer.Dispose(timerDisposed); timerDisposed.WaitOne();
timerDisposed.Dispose();
}
}
}

这样由于我们是在CreateAndStartTimer方法中给timer变量赋值了后,才启动Timer对象,所以当执行TimerCallBack事件处理方法时,timer变量就肯定不会为null了,TimerCallBack事件处理方法中执行timer.Change时,不会引发NullReferenceException异常。

System.Threading.Timer如何正确地被Dispose的更多相关文章

  1. System.Threading.Timer 定时器的用法

    System.Threading.Timer 是C# 中的一个定时器,可以定时(不断循环)执行一个任务.它是在线程上执行的,具有很好的安全性.为此  .Net Framework 提供了5个重载的构造 ...

  2. C# System.Threading.Timer 使用方法

    public class TimerHelper { System.Threading.Timer timer; public TaskSendMMS tasksendmms { get; set; ...

  3. System.Threading.Timer使用心得

    System.Threading.Timer 是一个使用回调方法的计时器,而且由线程池线程服务,简单且对资源要求不高. "只要在使用 Timer,就必须保留对它的引用."对于任何托 ...

  4. System.Threading.Timer的使用技巧

    转自:http://www.360doc.com/content/11/0812/11/1039473_139824496.shtml# System.Threading.Timer timer = ...

  5. c# 多线程之-- System.Threading Timer的使用

    作用:每隔多久去执行线程里的方法. class ThreadTimerDemo { static void Main(string[] args) { // Create an AutoResetEv ...

  6. C# System.Threading.Timer的使用

    using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threa ...

  7. .NET中System.Diagnostics.Stopwatch、System.Timers.Timer、System.Threading.Timer 的区别

    1.System.Diagnostics.Stopwatch Stopwatch 实例可以测量一个时间间隔的运行时间,也可以测量多个时间间隔的总运行时间. 在典型的 Stopwatch 方案中,先调用 ...

  8. C# 计时器用法(DispatcherTimer、System.Timers.Timer、System.Threading.Timer)

    首先,我觉得三种计时器最大的区别是:DispatcherTimer触发的内容会直接转到主线程去执行(耗时操作会卡住主线程),另外两个则是在副线程执行,如果需要修改界面,则需要手动转到主线程. Disp ...

  9. System.Threading.Timer 使用

    //定义计时器执行完成后的回调函数 TimerCallback timecallback = new TimerCallback(WriteMsg); //定义计时器 System.Threading ...

随机推荐

  1. .NET快速信息化系统开发框架 V3.2 -> WinForm“组织机构管理”界面组织机构权限管理采用新的界面,操作权限按模块进行展示

    对于某些大型的企业.信息系统,涉及的组织机构较多,模块多.操作权限也多,对用户或角色一一设置模块.操作权限等比较繁琐.我们可以直接对某一组织机构进行权限的设置,这样设置后,同一组织机构的用户就可以拥有 ...

  2. 4.计算机启动过程的简单介绍 计算机启动流程 计算机BIOS作用 POST 开机自检 计算机启动顺序 分区表 操作系统启动

    计算机的启动

  3. 【响应式编程的思维艺术】 (3)flatMap背后的代数理论Monad

    目录 一. 划重点 二. flatMap功能解析 三. flatMap的推演 3.1 函数式编程基础知识回顾 3.2 从一个容器的例子开始 3.3 Monad登场 3.4 对比总结 3.5 一点疑问 ...

  4. 细说MVC中仓储模式的应用

    文章提纲 概述要点 理论基础 详细步骤 总结 概述要点 设计模式的产生,就是在对开发过程进行不断的抽象. 我们先看一下之前访问数据的典型过程. 在Controller中定义一个Context, 例如: ...

  5. 36.QT-解决无边框界面拖动卡屏问题(附带源码)

    1.简介 看到很多才学QT的人都会问为啥无边框拖动为啥会花屏? 那是因为你每次拖动的过程中都一直在调用move()函数让QT重新绘制界面,如果资源过大,就会导致当前图形还未绘制完,便又重新改变坐标了, ...

  6. Java开发笔记(五十六)利用枚举类型实现高级常量

    前面介绍了联合利用final和static可实现常量的定义,该方式用于简单的常量倒还凑合,要是用于复杂的.安全性高的常量,那就力不从心了.例如以下几种情况,final结合static的方式便缺乏应对之 ...

  7. Vue UI:Vue开发者必不可少的工具

    译者按: Vue开发工具越来越好用了! 原文: Vue UI: A First Look 译者: Fundebug 本文采用意译,版权归原作者所有 随着最新的稳定版本Vue CLI 3即将发布,是时候 ...

  8. 利用efi功能更改bios主板被隐藏的设置(如超频)

    整理自(来源): http://tieba.baidu.com/p/4934345324 ([新手教程]利用EFI启动盘修改 隐藏bios设置) http://tieba.baidu.com/p/49 ...

  9. getDimension与getDimensionPixelOffset与getDimensionPixelSize的区别

    getDimension()                       返回float型px值     精确  getDimensionPixelOffset()     返回int型px值     ...

  10. Android为TV端助力 反编译

    http://blog.csdn.net/vipzjyno1/article/details/21039349/ apktool.bat d -f  test.apk  test     这条命令修改 ...