System.Threading.Timer如何正确地被Dispose
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的更多相关文章
- System.Threading.Timer 定时器的用法
System.Threading.Timer 是C# 中的一个定时器,可以定时(不断循环)执行一个任务.它是在线程上执行的,具有很好的安全性.为此 .Net Framework 提供了5个重载的构造 ...
- C# System.Threading.Timer 使用方法
public class TimerHelper { System.Threading.Timer timer; public TaskSendMMS tasksendmms { get; set; ...
- System.Threading.Timer使用心得
System.Threading.Timer 是一个使用回调方法的计时器,而且由线程池线程服务,简单且对资源要求不高. "只要在使用 Timer,就必须保留对它的引用."对于任何托 ...
- System.Threading.Timer的使用技巧
转自:http://www.360doc.com/content/11/0812/11/1039473_139824496.shtml# System.Threading.Timer timer = ...
- c# 多线程之-- System.Threading Timer的使用
作用:每隔多久去执行线程里的方法. class ThreadTimerDemo { static void Main(string[] args) { // Create an AutoResetEv ...
- C# System.Threading.Timer的使用
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threa ...
- .NET中System.Diagnostics.Stopwatch、System.Timers.Timer、System.Threading.Timer 的区别
1.System.Diagnostics.Stopwatch Stopwatch 实例可以测量一个时间间隔的运行时间,也可以测量多个时间间隔的总运行时间. 在典型的 Stopwatch 方案中,先调用 ...
- C# 计时器用法(DispatcherTimer、System.Timers.Timer、System.Threading.Timer)
首先,我觉得三种计时器最大的区别是:DispatcherTimer触发的内容会直接转到主线程去执行(耗时操作会卡住主线程),另外两个则是在副线程执行,如果需要修改界面,则需要手动转到主线程. Disp ...
- System.Threading.Timer 使用
//定义计时器执行完成后的回调函数 TimerCallback timecallback = new TimerCallback(WriteMsg); //定义计时器 System.Threading ...
随机推荐
- .NET快速信息化系统开发框架 V3.2 -> WinForm“组织机构管理”界面组织机构权限管理采用新的界面,操作权限按模块进行展示
对于某些大型的企业.信息系统,涉及的组织机构较多,模块多.操作权限也多,对用户或角色一一设置模块.操作权限等比较繁琐.我们可以直接对某一组织机构进行权限的设置,这样设置后,同一组织机构的用户就可以拥有 ...
- 4.计算机启动过程的简单介绍 计算机启动流程 计算机BIOS作用 POST 开机自检 计算机启动顺序 分区表 操作系统启动
计算机的启动
- 【响应式编程的思维艺术】 (3)flatMap背后的代数理论Monad
目录 一. 划重点 二. flatMap功能解析 三. flatMap的推演 3.1 函数式编程基础知识回顾 3.2 从一个容器的例子开始 3.3 Monad登场 3.4 对比总结 3.5 一点疑问 ...
- 细说MVC中仓储模式的应用
文章提纲 概述要点 理论基础 详细步骤 总结 概述要点 设计模式的产生,就是在对开发过程进行不断的抽象. 我们先看一下之前访问数据的典型过程. 在Controller中定义一个Context, 例如: ...
- 36.QT-解决无边框界面拖动卡屏问题(附带源码)
1.简介 看到很多才学QT的人都会问为啥无边框拖动为啥会花屏? 那是因为你每次拖动的过程中都一直在调用move()函数让QT重新绘制界面,如果资源过大,就会导致当前图形还未绘制完,便又重新改变坐标了, ...
- Java开发笔记(五十六)利用枚举类型实现高级常量
前面介绍了联合利用final和static可实现常量的定义,该方式用于简单的常量倒还凑合,要是用于复杂的.安全性高的常量,那就力不从心了.例如以下几种情况,final结合static的方式便缺乏应对之 ...
- Vue UI:Vue开发者必不可少的工具
译者按: Vue开发工具越来越好用了! 原文: Vue UI: A First Look 译者: Fundebug 本文采用意译,版权归原作者所有 随着最新的稳定版本Vue CLI 3即将发布,是时候 ...
- 利用efi功能更改bios主板被隐藏的设置(如超频)
整理自(来源): http://tieba.baidu.com/p/4934345324 ([新手教程]利用EFI启动盘修改 隐藏bios设置) http://tieba.baidu.com/p/49 ...
- getDimension与getDimensionPixelOffset与getDimensionPixelSize的区别
getDimension() 返回float型px值 精确 getDimensionPixelOffset() 返回int型px值 ...
- Android为TV端助力 反编译
http://blog.csdn.net/vipzjyno1/article/details/21039349/ apktool.bat d -f test.apk test 这条命令修改 ...