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 = 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異常。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM