所以这个时候我们需要一个机制来知道当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 { // 堆代码 duidaima.com static Timer timer = null; static ManualResetEvent timerDisposed = null;//ManualResetEvent继承WaitHandle static int timeCount = 0; static void CreateAndStartTimer() { //初始化Timer,设置触发间隔为2000毫秒,设置dueTime参数为Timeout.Infinite表示不启动Timer timer = new Timer(TimerCallBack, null, Timeout.Infinite, 2000); //启动Timer,设置dueTime参数为0表示立刻启动Timer timer.Change(0, 2000); } /// <summary> /// TimerCallBack方法是Timer每一次触发后的事件处理方法 /// </summary> static void TimerCallBack(object state) { //模拟做一些处理逻辑的事情 timeCount++;//每一次Timer触发调用TimerCallBack方法后,timeCount会加1 //当timeCount为100的时候,调用Timer.Change方法来改变Timer的触发间隔为1000毫秒 if (timeCount == 100) { timer.Change(0, 1000); } } 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上的解释如下:
using System; using System.Threading; namespace TimerDispose { class Program { static Timer timer = null; static ManualResetEvent timerDisposed = null;//ManualResetEvent继承WaitHandle static int timeCount = 0; static void CreateAndStartTimer() { // 堆代码 duidaima.com //初始化Timer,设置触发间隔为2000毫秒,设置dueTime参数为Timeout.Infinite表示不启动Timer timer = new Timer(TimerCallBack, null, Timeout.Infinite, 2000); //启动Timer,设置dueTime参数为0表示立刻启动Timer timer.Change(0, 2000); } /// <summary> /// TimerCallBack方法是Timer每一次触发后的事件处理方法 /// </summary> static void TimerCallBack(object state) { //模拟做一些处理逻辑的事情 timeCount++;//每一次Timer触发调用TimerCallBack方法后,timeCount会加1 //当timeCount为100的时候,调用Timer.Change方法来改变Timer的触发间隔为1000毫秒 if (timeCount == 100) { //添加try catch代码块,来捕捉Timer.Change方法抛出的ObjectDisposedException异常 try { timer.Change(0, 1000); } 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异常使整个程序报错终止,至少异常抛出时我们是有代码去处理的。
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; } } } } }可以参考这个链接查看详情,需要注意的是里面有说到几点:
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方法会一直阻塞下去。
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, 0, 2000); } /// <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销毁,而这可能并不是我们期望的行为。
using System; using System.Threading; namespace TimerDispose { class Program { //变量timer,用于引用CreateAndStartTimer方法内部创建的Timer对象 static Timer timer = null; static void CreateAndStartTimer() { //堆代码 duidaima.com //初始化Timer,设置触发间隔为2000毫秒,设置dueTime参数为Timeout.Infinite表示不启动Timer //将创建的Timer对象,指定给一个程序全局都可以访问到的变量timer,防止Timer对象被.NET的垃圾回收机制GC销毁 timer = new Timer(TimerCallBack, null, Timeout.Infinite, 2000); //启动Timer,设置dueTime参数为0表示立刻启动Timer timer.Change(0, 2000); } /// <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,设置触发间隔为2000毫秒,设置dueTime参数为Timeout.Infinite表示不启动Timer timer = new Timer(TimerCallBack, null, Timeout.Infinite, 2000); //启动Timer,设置dueTime参数为0表示立刻启动Timer timer.Change(0, 2000); 那么我们为什么不将初始化和启动Timer在一行代码中完成呢,如下所示: //初始化并启动Timer,设置触发间隔为2000毫秒,设置dueTime参数为0表示立刻启动Timer timer = new Timer(TimerCallBack, null, 0, 2000);要解释这个问题,我们先来看看下面的代码:
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, 0, 2000); } /// <summary> /// TimerCallBack方法是Timer每一次触发后的事件处理方法 /// </summary> static void TimerCallBack(object state) { //模拟做一些处理逻辑的事情 //调用Timer.Change方法来改变Timer的触发间隔为1000毫秒 //由于调用下面timer.Change方法时,可能CreateAndStartTimer方法中的timer变量还没有被赋值,timer变量为null,所以会引发NullReferenceException异常 timer.Change(0, 1000); } 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异常。
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, 2000); //启动Timer,设置dueTime参数为0表示立刻启动Timer,此时timer变量肯定不会为null了 timer.Change(0, 2000); } /// <summary> /// TimerCallBack方法是Timer每一次触发后的事件处理方法 /// </summary> static void TimerCallBack(object state) { //模拟做一些处理逻辑的事情 //调用Timer.Change方法来改变Timer的触发间隔为1000毫秒 //由于调用下面timer.Change方法时,timer变量已经被赋值不为null,所以不会引发NullReferenceException异常 timer.Change(0, 1000); } 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异常。