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 = 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上的解釋如下:
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毫秒,設置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異常使整個程序報錯終止,至少異常拋出時我們是有代碼去處理的。
而國外的一位高手不僅考慮到了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, 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毫秒,設置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,再啟動Timer
上面的代碼中可以看到,我們都是先初始化Timer,再啟動Timer,如下所示:
//初始化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異常。
因此我們必須要保證當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, 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異常。