.net中Timer 如何正确地被Dispose

作者 : 慕源网 本文共8836个字,预计阅读时间需要23分钟 发布时间: 2021-09-21 共353人阅读

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

.net中Timer 如何正确地被Dispose

我们启动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 = 0;
       static void CreateAndStartTimer()
       
{
           //初始化Timer,设置触发间隔为2000毫秒
           timer = new Timer(TimerCallBack, null, 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上的解释如下:

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 = 0;
       static void CreateAndStartTimer()
       
{
           //初始化Timer,设置触发间隔为2000毫秒
           timer = new Timer(TimerCallBack, null, 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异常,至少异常抛出时我们是有代码去处理的。

而国外的一位高手不仅考虑到了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;
               }
           }
       }
   }
}

 

可以参考这个链接(https://stackoverflow.com/questions/13396440/c-sharp-system-threading-timer-wait-for-dispose)查看详情,需要注意的是里面有说到几点:

第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,否则WaitHandle.WaitOne方法会一直阻塞下去。

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

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

using System;
using System.Threading;
namespace TimerDispose
{
   class Program
   {
       static void CreateAndStartTimer()
       
{
           //初始化Timer,设置触发间隔为2000毫秒
           //由于我们这里创建的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销毁,而这可能并不是我们期望的行为。

对此有如下解决方案:

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

using System;
using System.Threading;
namespace TimerDispose
{
   class Program
   {
       //变量timer,用于引用CreateAndStartTimer方法内部创建的Timer对象
       static Timer timer = null;
       static void CreateAndStartTimer()
       
{
           //初始化Timer,设置触发间隔为2000毫秒
           //将创建的Timer对象,指定给一个程序全局都可以访问到的变量timer,防止Timer对象被.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对象,现在可以通过变量timer被整个程序访问到,所以就不会被.NET的垃圾回收机制GC销毁掉了。

 


慕源网 » .net中Timer 如何正确地被Dispose

常见问题FAQ

程序仅供学习研究,请勿用于非法用途,不得违反国家法律,否则后果自负,一切法律责任与本站无关。
请仔细阅读以上条款再购买,拍下即代表同意条款并遵守约定,谢谢大家支持理解!

发表评论

开通VIP 享更多特权,建议使用QQ登录