C# System.Threading.Timer 詳解及範例

2023-02-23 12:00:17

前言

定時器功能在日常開發中也是比較常用的,在 .Net 中實際上總共有五種定時器,分別是:System.Timers.TimerSystem.Threading.TimerSystem.Windows.Forms.TimerSystem.Web.UI.Timer (僅 .NET Framework)、System.Windows.Threading.DispatcherTimer

其中最常用的就是 System.Threading.Timer 基於執行緒池的定時器,相較於另外幾種定時器,其安全性較高,適用性最強,因此本文將詳細介紹此定時器的相關內容。

一、兩類過載

參考:Timer 建構函式

1、 Timer(TimerCallback)

使用新建立的 Timer 物件作為狀態物件,用一個無限週期和一個無限到期時間初始化 Timer 類的新範例。當迴圈任務達成時,可以在回撥函數中將當前的 Timer 物件釋放掉。

// 語法
public Timer (System.Threading.TimerCallback callback);

下面是一個簡單範例:(在回撥函數 TimerProc 中,我們可以通過將 Timer 物件釋放掉,來結束迴圈過程)

using System;
using System.Threading;

namespace Test.Test.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Program ex = new Program();
            ex.StartTimer(4000); // 建立兩個 Timer 物件
            ex.StartTimer(1000);
            Console.WriteLine("Press Enter to end the program.");
            Console.ReadLine();
        }
        public void StartTimer(int dueTime)
        {
            Timer t = new Timer(new TimerCallback(TimerProc));
            t.Change(dueTime, 5000); // 啟動定時器
            // dueTime:表示延遲呼叫的時間;
            // 5000:表示回撥的時間間隔,單位:毫秒
            // 如果在回撥中將 Timer 釋放掉,則後續回撥將無法發生
        }
        private void TimerProc(object state) // 入參物件為 Timer 物件
        {
            Timer t = (Timer)state;
            t.Dispose(); // 呼叫一次就釋放掉,或者新增條件釋放
            Console.WriteLine("The timer callback executes.");
        }
    }
}
// 輸出結果:
// Press Enter to end the program.
// The timer callback executes.
// The timer callback executes.

2、Timer(TimerCallback, Object, Int32, Int32)

使用 32 位的有符號整數指定時間間隔,初始化 Timer 類的新範例。callback:回撥函數名;state:包含回撥方法要使用的資訊的狀態物件,可為空;dueTime:延遲呼叫的時間;period:重複回撥的時間間隔。

// 語法
public Timer (System.Threading.TimerCallback callback, object? state, int dueTime, int period);

下面是一個範例:(關於執行緒自動重置類:AutoResetEvent 類

通過執行緒同步事件,演示不同時間間隔輸出結果的區別
using System;
using System.Threading;

class Program
{
    static void Main()
    {
        // AutoResetEvent:表示一個執行緒同步事件,在等待執行緒釋放後,收到訊號時,自動重置
        var autoEvent = new AutoResetEvent(false);
        var statusChecker = new StatusChecker(10); // 範例化 StatusChecker 並設定最大回圈次數
        Console.WriteLine($"{DateTime.Now.ToString("HH:mm:dd.fff")} Creating timer.\n");
        var stateTimer = new Timer(statusChecker.CheckStatus, autoEvent, 1000, 250);
        // 1000 表示延遲 1s 開始執行;250 表示回撥時間間隔為 0.25s
        autoEvent.WaitOne(); // WaitOne:阻塞當前執行緒,直到 WaitHandle 接收到訊號
        stateTimer.Change(0, 500); // Change:0 不延遲立即啟動執行;500 表示回撥間隔為 0.5s
        Console.WriteLine("\nChanging period to .5 seconds.\n");
        autoEvent.WaitOne(); // WaitOne:阻塞當前執行緒,直到 WaitHandle 接收到訊號
        stateTimer.Dispose(); // 釋放 Timer 物件
        Console.WriteLine("\nDestroying timer.");
    }
}
class StatusChecker
{
    private int invokeCount; // 回撥計數
    private int maxCount; // 回撥最大次數
    public StatusChecker(int count)
    {
        invokeCount = 0;
        maxCount = count;
    }
    public void CheckStatus(Object stateInfo)
    {
        AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;
        Console.WriteLine($"{DateTime.Now.ToString("HH: mm:ss.fff")} Checking status {(++invokeCount)}.");
        if (invokeCount == maxCount)
        {
            invokeCount = 0;
            // Set 將事件狀態設定為有訊號狀態,允許一個或多個等待執行緒繼續執行
            autoEvent.Set();
        }
    }
}
// 輸出結果:
// 11:26:22.571 Creating timer.
// 
// 11:26:16.489 Checking status  1.
// 11:26:16.500 Checking status  2.
// 11:26:16.749 Checking status  3.
// 11:26:17.000 Checking status  4.
// 11:26:17.245 Checking status  5.
// 11:26:17.503 Checking status  6.
// 11:26:17.744 Checking status  7.
// 11:26:17.993 Checking status  8.
// 11:26:18.241 Checking status  9.
// 11:26:18.504 Checking status 10.
// 
// Changing period to .5 seconds.
// 
// 11:26:18.505 Checking status  1.
// 11:26:19.017 Checking status  2.
// 11:26:19.510 Checking status  3.
// 11:26:20.009 Checking status  4.
// 11:26:20.509 Checking status  5.
// 11:26:21.020 Checking status  6.
// 11:26:21.518 Checking status  7.
// 11:26:22.016 Checking status  8.
// 11:26:22.516 Checking status  9.
// 11:26:23.016 Checking status 10.
// 
// Destroying timer.

與此過載類似用法的另外三個過載如下:

// 1、用 64 位整數表示時間間隔
Timer(TimerCallback, Object, Int64, Int64)
// 2、時間戳引數
//  時間戳語法:public TimeSpan (int hours, int minutes, int seconds);
//  TimeSpan delayTime = new TimeSpan(0, 0, 1); 
//  時間戳語法:public TimeSpan (int days, int hours, int minutes, int seconds, int milliseconds, int microseconds);
//  TimeSpan intervalTime = new TimeSpan(0, 0, 0, 0, 250);
Timer(TimerCallback, Object, TimeSpan, TimeSpan)
// 3、用 32 位無符號整數來表示時間間隔
Timer(TimerCallback, Object, UInt32, UInt32)

二、屬性 ActiveCount

獲取當前活動的計時器數量。 活動計數器定義為,在未來某一時間點觸發且尚未取消。

下面是一個簡單的範例:

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        var stateTimer = new Timer((para)=>{ }, null, 1000, 250);
        var stateTimer2 = new Timer((para) => { }, null, 1000, 250);
        Console.WriteLine($"1、Timer ActiveCount.{Timer.ActiveCount}");
        stateTimer.Dispose(); // 釋放 Timer 物件
        Console.WriteLine($"\n2、Timer ActiveCount.{Timer.ActiveCount}");
        stateTimer2.Dispose(); // 釋放 Timer2 物件
        Console.WriteLine($"\n3、Timer ActiveCount.{Timer.ActiveCount}");
        Thread.Sleep(5000);
    }
}
// 輸出結果:
// 1、Timer ActiveCount.2
// 
// 2、Timer ActiveCount.1
// 
// 3、Timer ActiveCount.0

三、方法

1、Timer.Change 方法

更改計時器的延遲啟動時間和方法迴圈呼叫之間的時間間隔。單位均為毫秒(ms)。

其和 Timer 的建構函式過載類似,都是有四個過載,之間只有引數不同,用法相同。

四種型別分別是:Int32(32 位正整數)、Int64(64 位正整數)、TimeSpan(時間戳)、UInt32(32 位無符號整數)。

下面例舉一個時間為正整數的範例:

// 先建立一個 Timer 物件
var stateTimer = new Timer((para)=>{ }, null, 1000, 250);
// 呼叫變更物件的方法如下:1000 表示:延遲 1s 觸發;500 表示間隔 0.5s 迴圈呼叫
stateTimer.Change(1000, 500);

2、Timer.Dispose 方法

此方法共有兩個過載,分別是:Dispose()、Dispose(WaitHandle)。

Dispose()	
// 釋放由 Timer 範例使用的當前所有資源
Dispose(WaitHandle)	
// 釋放由 Timer 範例使用的當前所有資源,並在釋放完成時發出訊號

Dispose() 方法就是直接將 Timer 物件釋放調,這裡就不再贅述了,下面來看一個關於 Dispose(WaitHandle) 的範例:

using System;
using System.Threading;

namespace TimerDispose
{
    class Program
    {
        static Timer timer = null; //**宣告一個全域性變數,避免 Timer 物件後續沒有呼叫時,被 GC回收
        // ManualResetEvent 繼承自 WaitHandle
        // 是否手動重置事件(是 WaitHandle 的子類) false 初始狀態為無訊號 true 初始狀態為有訊號
        static ManualResetEvent timerDisposed = null;
        static void CreateAndStartTimer()
        {
            // 初始化 Timer,設定觸發間隔為 2000 毫秒,設定 dueTime 引數為 Timeout.Infinite 表示不啟動 Timer
            timer = new Timer(TimerCallBack, null, Timeout.Infinite, 2000);
            // 啟動 Timer,設定 dueTime 引數為 0 表示立刻啟動 Timer
            //**先範例化再啟動的目的是:避免在呼叫 Dispose 方法前,timer 物件還未完成賦值,所導致的空物件錯誤
            timer.Change(0, 2000);
        }
        /// <summary>
        /// TimerCallBack 方法是 Timer 每一次觸發後的事件處理方法
        /// </summary>
        static void TimerCallBack(object state)
        {
            // Thread.Sleep(10000); // Change() 報錯:System.ObjectDisposedException: 'Cannot access a disposed object.'
            try
            {
                timer.Change(0, 1000);
            }
            catch (ObjectDisposedException) //**當 Timer 物件已經呼叫了 Dispose 方法後,再呼叫 Change 方法,會丟擲 ObjectDisposedException 異常
            {
                Console.WriteLine("在 Timer.Dispose 方法執行後,再呼叫 Timer.Change 方法已經沒有意義");
            }
            Thread.Sleep(10000); // 在 Change() 之後可正常執行
        }
        static void Main(string[] args)
        {
            CreateAndStartTimer();
            Console.WriteLine("按任意鍵呼叫Timer.Dispose方法...");
            Console.ReadKey();
            timerDisposed = new ManualResetEvent(false);
            // 呼叫 Timer 的 bool Dispose(WaitHandle notifyObject) 過載方法,來結束Timer的觸發,
            // 當執行緒池中的所有 TimerCallBack 方法都執行完畢後,Timer 會發一個訊號給 timerDisposed
            timer.Dispose(timerDisposed);
            // WaitHandle.WaitOne() 方法會等待收到一個訊號,否則一直被阻塞
            timerDisposed.WaitOne();
            timerDisposed.Dispose();
            Console.WriteLine("Timer已經結束,按任意鍵結束整個程式...");
            Console.ReadKey();
        }
    }
}

另外一個很不錯的範例,是一個國外的高手所寫,不僅考慮到了 Timer.Change 方法會丟擲 ObjectDisposedExceptio n異常,他還給 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;
                }
            }
        }
    }
}

參考:System.Threading.Timer如何正確地被Dispose

3、Timer.DisposeAsync 方法

其為上一部分的一種非同步實現。

從 .NET Core 開始,就意味著 .NET 來到了一個全新的非同步時代。無論是各種基礎類庫(比如 System.IO)、AspNet Core、還是 EFCore 等等,它們都逐漸支援非同步操作。其不阻止執行緒的執行,帶來高效能的同時還基本不需要更改原有的編碼習慣,因此後續針對非同步的程式設計肯定會越來越普遍。

當一個實體類同時實現了 Dispose 和 DisposeAsync,由於程式會先判斷時候實現了 DisposeAsync 非同步釋放,所以一般優先呼叫非同步釋放。參考:熟悉而陌生的新朋友——IAsyncDisposable

終極參考:Timer 類