.NET中有多少種定時器

2023-11-28 21:03:40

.NET中至少有6種定時器,每一種定時器都有它的用途和特點。根據定時器的應用場景,可以分為UI相關的定時器和UI無關的定時器。本文將簡單介紹這6種定時器的基本用法和特點。

UI定時器

.NET中的UI定時器主要是WinForm、WPF以及WebForm中的定時器。分別為:

  • System.Windows.Forms.Timer
  • System.Windows.Threading.DispatcherTimer
  • System.Web.UI.Timer

通常情況下,WinForm、WPF中的定時器是在UI執行緒上執行回撥函數,因此可以直接存取UI元素。由於WinForm、WPF支援單執行緒單元模型(Single-Thread Apartment,STA),定時器間隔事件是在UI執行緒上觸發,因此,不用擔心執行緒安全問題。
System.Web.UI.Timer是通過Javascript定時器和伺服器端非同步回撥實現,也是單執行緒的。

請注意,這裡說的是通常情況,後邊介紹System.Windows.Threading.DispatcherTimer時會提到在非UI執行緒建立DispatcherTimer時也無法直接存取UI元素。

System.Windows.Forms.Timer

System.Windows.Forms.Timer針對WinForm應用進行了優化,是隻能在WinForm上使用的定時器。這個定時器是針對單執行緒環境設計的,是在UI執行緒上處理定時任務。
它要求使用者程式碼有可用的UI訊息泵,定時任務須在UI執行緒上執行,或者跨執行緒通過Invoke或者BeginInvoke封送(marshal)到UI執行緒上執行。其優點是使用簡單,只需通過給Interval屬性賦值來設定時間間隔,並註冊Tick事件處理定時任務。其缺點是精度不高,精度為55毫秒,也就是Interval賦值小於55時,也是55毫秒觸發一次定時任務。

public partial class TimerFrom : Form
{
    private System.Windows.Forms.Timer digitalClock;
    private void TimerFrom_Load(object sender, EventArgs e)
    {
        digitalClock = new System.Windows.Forms.Timer();//建立定時器 
        digitalClock.Tick += new EventHandler(HandleTime);//註冊定時任務事件 
        digitalClock.Interval = 1000;//設定時間間隔
        digitalClock.Enabled = true;
        digitalClock.Start(); //開啟定時器
    }
    public void HandleTime(Object myObject, EventArgs myEventArgs)
    {
        labelClock.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
    }
    private void frmTimerDemo_FormClosed(object sender, FormClosedEventArgs e)
    {
        digitalClock.Stop();//停止定時器
        digitalClock.Dispose();
    }
}

System.Windows.Threading.DispatcherTimer

System.Windows.Threading.DispatcherTimer是WPF中的定時器,它是基於Dispatcher物件的(並不是基於UI執行緒的)。DispatcherTimer的定時任務是像其他操作一樣放在Dispatcher佇列上,其執行操作時間依賴於佇列中其他任務及其優先順序,因此,DispatcherTimer不保證在時間間隔發生時準確執行,只保證不會在時間間隔發生前執行。

Dispatcher為特定執行緒維護工作項(操作)的優先順序佇列,線上程上建立Dispatcher物件時,它成為唯一可以關聯該執行緒的Dispatcher物件,WPF中, DispatcherObject只能被與之關聯的Dispatcher物件存取,也就是非UI執行緒中無法直接存取UI元素(WPF中的UI元素都是派生自 DispatcherObject

此外,DispatcherTimer不像System.Windows.Forms.Timer那樣只在UI執行緒上建立才能觸發Tick事件,它在非UI執行緒下建立也可以觸發Tick事件,此時存取UI元素也需要通過Invoke或者BeginInvoke封送(marshal)到UI執行緒上執行。其優點也是簡單易用,適合在UI執行緒上執行任務或觸發事件,缺點是精度不準確,可能存在延遲。

private void Dt_Tick(object sender, EventArgs e)
{
    Dispatcher.BeginInvoke((Action)delegate ()
    {
        text1.Text = DateTime.Now.ToString();
    });
    Console.WriteLine(DateTime.Now.ToString());
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    Task.Run(() =>{
        DispatcherTimer dt = new DispatcherTimer();
        dt.Tick += Dt_Tick;
        dt.Interval = TimeSpan.FromSeconds(1);
        dt.Start();
        Dispatcher.Run();
    });
}

上述程式碼中,DispatcherTimer是非UI執行緒中建立,定時任務中存取UI元素text1,需要通過Invoke或者BeginInvoke封送(marshal)到UI執行緒上執行,而Console.WriteLine則可以直接執行。

System.Web.UI.Timer

System.Web.UI.Timer是僅適用於.NET FrameworkASP.NET元件。通過Javascript定時器和伺服器端非同步回撥實現。每次觸發定時器時,只能執行一個非同步回撥方法,而其他的非同步回撥方法需要等待前一個非同步回撥方法執行完畢後才能執行。這樣可以保證在任意時刻只有一個非同步回撥方法在執行,避免了多執行緒並行執行的問題。

UI無關定時器

從 .NET 6開始,UI無關定時器有三個:

  • System.Threading.Timer
  • System.Timers.Timer
  • System.Threading.PeriodicTimer(.NET 6+)

System.Threading.Timer

System.Threading.Timer是最基礎輕量的定時器,它將定期線上程池執行緒上執行單個回撥方法。在建立定時器物件時必須指定回撥方法,並且後續不能修改,同時也可以指定定時器回撥開始執行的時間以及時間間隔。定時器建立後可以通過Change方法修改回撥開始執行的時間以及時間間隔。該定時器的優點是輕量,精度相對較高,與Windows作業系統時鐘精度一致,大約15毫秒。但因為是基於執行緒池的,所以在任務執行時間較長或者執行緒池過載時,會出現延遲。其缺點是使用不太方便,定時器建立後無法修改回撥方法。

var stateTimer = new 
var autoEvent = new AutoResetEvent(false);
Timer(CheckStatus, autoEvent, 1000,250);

private int invokeCount=0;

public void CheckStatus(Object stateInfo)
{
    AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;
    Console.WriteLine("{0} Checking status {1,2}.",DateTime.Now.ToString("h:mm:ss.fff"),(++invokeCount).ToString());

    if(invokeCount == 10)
    {
        invokeCount = 0;
        autoEvent.Set();
    }
}

System.Timers.Timer

System.Timers.Timer在內部使用System.Threading.Timer,並公開了更多的屬性,如AutoReset, EnabledSynchronizingObject,這些屬性允許設定回撥的執行方式。此外,Tick事件允許註冊多個處理程式。因此,一個定時器可以觸發多個處理程式。還可以在計時器啟動後更改處理程式。與System.Threading.Timer相似,其優點也是精度相對較高,與Windows作業系統時鐘精度一致,大約15毫秒。因為預設(或者SynchronizingObject=null時)是基於執行緒池的,所以在任務執行時間較長或者執行緒池過載時,會出現延遲。但使用要更簡便一些。

public partial class TimerFrom : Form
{
    private System.Timers.Timer timer;
    private void TimerFrom_Load(object sender, EventArgs e)
    {
        // 支援註冊多個處理程式
        timer.Elapsed += (sender, e) => { label1.Text = DateTime.Now.ToLongTimeString(); };
        timer.Elapsed += (sender, e) => { Console.WriteLine(DateTime.Now.ToLongTimeString()); };
        //自定義回撥執行的方式(指定物件所在的執行緒),SynchronizingObject=null時線上程池上執行
        timer.SynchronizingObject = this;
        timer.AutoReset = true;
        timer.Start();
    }
}

本例中將SynchronizingObject屬性設定為Form物件,因此Elapsed的處理程式在UI執行緒上執行,可以直接修改 label1.Text,如果SynchronizingObject屬性為null,處理程式則是線上程池執行緒上執行,修改 label1.Text時需要通過Invoke或者BeginInvoke封送(marshal)到UI執行緒上執行。

System.Threading.PeriodicTimer

System.Threading.PeriodicTimer是 .NET 6中引入的定時器。它能方便地使用非同步方式,它沒有Tick事件,而是提供WaitForNextTickAsync方法處理定時任務。通常是使用While迴圈結合CancellationToken一起使用。和CancellationToken一起用的時候需要注意,如果CancellationToken被取消的時候會丟擲一個OperationCanceledException需要考慮自己處理異常。相比之前的定時器來說,有下面幾個特點:[1]

  1. 沒有callback 來繫結事件;
  2. 不會發生重入,只允許有一個消費者,不允許同一個PeriodicTimer在不同的地方同時WaitForNextTickAsync,不需要自己做排他鎖來實現不能重入;
  3. 非同步化。之前的 timer 的 callback 都是同步的,使用新 timer 可以使用非同步方法,避免了編寫 Sync over Async 程式碼;
  4. Dispose 之後,範例就無法使用,並且 WaitForNextTickAsync 始終返回 false。
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
using (var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)))
{
    try
    {
        while (await timer.WaitForNextTickAsync(cts.Token))
        {
            await Task.Delay(3000);
            Console.WriteLine($"ThreadId is {Thread.CurrentThread.ManagedThreadId} --- Time is {DateTime.Now:HH:mm:ss}");
        }
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Operation cancelled");
    }
}

小結

我們在開發過程中遇到的坑往往不是技術本身的坑,而是我們濫用沒有掌握的技術導致的,在有多種技術方案可選的時候,通常只關注技術的優點,忽略了技術適用場景及其侷限性。.NET中幾種定時器各自都有其適用場景和不足,但都不支援高精度計時。瞭解這些有助於我們在開發過程中選擇合適定時器,避免遇到問題後被動地替換解決方案。


  1. https://xie.infoq.cn/article/6aa23b6850abddf717a6c9fc9 ↩︎