幾個常見的定時器


NET允許不同的命名空間里存在同名的類——“System.Timers.Timer, System.Threading.Timer和Sytem.Windows.Forms.Timer”就是一個很好的例子。那么它們之間有何區別呢?我們這就來分析一下:

[1]System.Windows.Forms.Timer:這個Timer是我們最最常見的一個Timer,主要用於一般Windows窗體編程時候的定時使用。(主要的屬性:Interval——用於控制每隔多少時間出發一次Tick事件,Enabled——是否立即啟動定時器,Tick事件——用於在Interval到時之后觸發的行為)。

由於該Timer是相對整個WinForm(UI)上的異步操作,因此可以直接控制WinForm上的任何控件;不過缺陷在於既然是基於UI界面的主線程的異步操作,這將導致如果Timer執行一個長時間的任務,會導致界面死亡,這也就證明了Timer並非是我們一些人所謂的“多線程”(注意:筆者在這里“多線程”指代並行運行的線程,或者不會引發UI死掉的線程)。我們可以嘗試這個代碼(在Tick事件中):

[C#]

Thread.Sleep(10000);

[VB.NET]

Thread.Sleep(10000)

啟動定時器之后,發現界面會假死10秒左右的時間。另外時間不一定精准(因為基於UI異步,或許會有延時等問題)。

[2]System.Threading.Timer:

該Timer是一個基於ThreadPool構建的Timer,相對第一個而言最大的優勢在於不占用UI線程,也就是真正的“並行”多線程;再者由於是使用ThreadPool,自然其內部通過線程池管理線程,可以達到資源的最大化。不過通過MSDN上我們注意到一個“劣勢”:

System.Threading.Timer 是一個簡單的輕量計時器,它使用回調方法並由線程池線程提供服務。 不建議將其用於 Windows 窗體,因為其回調不在用戶界面線程上進行。 System.Windows.Forms.Timer 是用於 Windows 窗體的更佳選擇。

這句話告訴讀者說第二個Timer(也就是我現在這段說的Timer)不適合用於WinForm,所以建議讀者使用第一種Timer。我對這句話有些“嗤之以鼻”的(雖然我承認絕大部分MSDN的確相當不錯!)——究其原因,是因為既然該Timer基於ThreadPool,如果操作了WinForm的Control,在調試的時候會拋出“試圖從非創建該控件的線程中去訪問該控件”的異常(這個問題我的以前一篇博文涉及到過)。我們為何不能使用控件的Invoke呢?改造一下:

[C#]

public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            System.Threading.Timer t = new System.Threading.Timer
                (new System.Threading.TimerCallback((obj) =>
                {
                    this.Invoke(new MethodInvoker(() => { label1.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); }));
                }), null, 0, 1000);
        }
    }

[VB.NET]

Public Partial Class Form1
    Inherits Form
    Public Sub New()
        InitializeComponent()
    End Sub

    Private Sub button1_Click(sender As Object, e As EventArgs)
        Dim t As New System.Threading.Timer(New System.Threading.TimerCallback(Function(obj) 
        Me.Invoke(New MethodInvoker(Function() 
        label1.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
End Function))
End Function), Nothing, 0, 1000)
    End Sub
End Class

之所以用Me.Invoke,這是為了避免用非創建該控件的線程去調用該控件所引發的錯誤。

另外該Timer沒有Start什么的啟動方法,初始化之后就自動啟動,其最原始的構造函數如下:

[C#]

public Timer(
    TimerCallback callback,
    Object state,
    int dueTime,
    int period
)

[VB.NET]

Public Sub New ( _
    callback As TimerCallback, _
    state As Object, _
    dueTime As Integer, _
    period As Integer _
)

其中CallBack是構造函數(回調函數,相當於Tick事件),state是需要操作的對象(一般為空,不需要),dueTime:隔多少毫秒之后第一次觸發回調函數(>0,如果=0,初始化后立即回調函數執行,<0,停止),period:每隔多少時間執行重復執行回調函數(>0,如果<=0,只執行一次回調函數,如果dueTime>0的話)。

[3]System.Timers.Timer:

這個類一般用於服務端(譬如Windows服務等),最大的特色在於它“集成”了1和2的特點——

3.1)基於ThreadPool:因此和[2]的那個Timer一樣,必須用Invoke的方法方可在WinForm中使用。示例代碼如下:

[C#]

public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            System.Timers.Timer t = new System.Timers.Timer(1000);
            t.Elapsed += t_Elapsed;
            t.AutoReset = true;       
            t.Start();
        }

        void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            if (!this.InvokeRequired)
            {
                label1.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            }
            else
            {
                this.Invoke(new MethodInvoker(() => { label1.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); }));
            }
        }
    }

[VB.NET]

Public Partial Class Form1
    Inherits Form
    Public Sub New()
        InitializeComponent()
    End Sub

    Private Sub button1_Click(sender As Object, e As EventArgs)
        Dim t As New System.Timers.Timer(1000)
        AddHandler t.Elapsed, AddressOf t_Elapsed
        t.AutoReset = True
        t.Start()
    End Sub

    Private Sub t_Elapsed(sender As Object, e As System.Timers.ElapsedEventArgs)
        If Not Me.InvokeRequired Then
            label1.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
        Else
            Me.Invoke(New MethodInvoker(Function() 
            label1.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")

End Function))
        End If
    End Sub
End Class

3.2)有可視化的界面,但是默認不會加載到控件庫中,必須手動添加方可:

這里大家請注意一點——一旦我們使用該可視化的System.Timers.Timer,其實效果是和[1]的那個幾乎一樣的,失去了ThreadPool的優勢。究其原因在於你把該控件拖拽到了WinForm上,后台代碼會在上面類似3.1中代碼中額外地增加一條“t.SynchronizingObject = this;”(VB.NET: t.SynchronizingObject = Me),這個"SynchronizingObject"默認是null,表示從線程池引發間隔的時間行為;但是如果設置成了this(窗體),則變成了從窗體的UI上每隔一定時間觸發一次事件行為。看反射后的代碼更有助於我們了解內部狀況(有刪節):

[C#]

[DefaultEvent("Elapsed"), DefaultProperty("Interval"), HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]
public class Timer : Component, ISupportInitialize
{
    // Fields
    private bool autoReset;
    private TimerCallback callback;
    private object cookie;
    private bool delayedEnable;
    private bool disposed;
    private bool enabled;
    private bool initializing;
    private double interval;
    private ISynchronizeInvoke synchronizingObject;
    private Timer timer;

    // Events
    [Category("Behavior"), TimersDescription("TimerIntervalElapsed")]
    public event ElapsedEventHandler Elapsed;

    // Methods
    public Timer()
    {
        this.interval = 100.0;
        this.enabled = false;
        this.autoReset = true;
        this.initializing = false;
        this.delayedEnable = false;
        this.callback = new TimerCallback(this.MyTimerCallback);
    }

    public Timer(double interval) : this()
    {
        if (interval <= 0.0)
        {
            throw new ArgumentException(SR.GetString("InvalidParameter", new object[] { "interval", interval }));
        }
        double num = Math.Ceiling(interval);
        if ((num > 2147483647.0) || (num <= 0.0))
        {
            throw new ArgumentException(SR.GetString("InvalidParameter", new object[] { "interval", interval }));
        }
        this.interval = (int) num;
    }

  
    private void MyTimerCallback(object state)
    {
        if (state == this.cookie)
        {
            if (!this.autoReset)
            {
                this.enabled = false;
            }
            FILE_TIME lpSystemTimeAsFileTime = new FILE_TIME();
            GetSystemTimeAsFileTime(ref lpSystemTimeAsFileTime);
            ElapsedEventArgs e = new ElapsedEventArgs(lpSystemTimeAsFileTime.ftTimeLow, lpSystemTimeAsFileTime.ftTimeHigh);
            try
            {
                ElapsedEventHandler onIntervalElapsed = this.onIntervalElapsed;
                if (onIntervalElapsed != null)
                {
                    if ((this.SynchronizingObject != null) && this.SynchronizingObject.InvokeRequired)
                    {
                        this.SynchronizingObject.BeginInvoke(onIntervalElapsed, new object[] { this, e });
                    }
                    else
                    {
                        onIntervalElapsed(this, e);
                    }
                }
            }
            catch
            {
            }
        }
    }

 ………………
}

[VB.NET]

<DefaultEvent("Elapsed"), DefaultProperty("Interval"), HostProtection(SecurityAction.LinkDemand, Synchronization := True, ExternalThreading := True)> _
Public Class Timer
    Inherits Component
    Implements ISupportInitialize
    ' Fields
    Private autoReset As Boolean
    Private callback As TimerCallback
    Private cookie As Object
    Private delayedEnable As Boolean
    Private disposed As Boolean
    Private enabled As Boolean
    Private initializing As Boolean
    Private interval As Double
    Private synchronizingObject As ISynchronizeInvoke
    Private timer As Timer

    ' Events
    <Category("Behavior"), TimersDescription("TimerIntervalElapsed")> _
    Public Event Elapsed As ElapsedEventHandler

    ' Methods
    Public Sub New()
        Me.interval = 100.0
        Me.enabled = False
        Me.autoReset = True
        Me.initializing = False
        Me.delayedEnable = False
        Me.callback = New TimerCallback(AddressOf Me.MyTimerCallback)
    End Sub

    Public Sub New(interval As Double)
        Me.New()
        If interval <= 0.0 Then
            Throw New ArgumentException(SR.GetString("InvalidParameter", New Object() {"interval", interval}))
        End If
        Dim num As Double = Math.Ceiling(interval)
        If (num > 2147483647.0) OrElse (num <= 0.0) Then
            Throw New ArgumentException(SR.GetString("InvalidParameter", New Object() {"interval", interval}))
        End If
        Me.interval = CInt(Math.Truncate(num))
    End Sub


    Private Sub MyTimerCallback(state As Object)
        If state Is Me.cookie Then
            If Not Me.autoReset Then
                Me.enabled = False
            End If
            Dim lpSystemTimeAsFileTime As New FILE_TIME()
            GetSystemTimeAsFileTime(lpSystemTimeAsFileTime)
            Dim e As New ElapsedEventArgs(lpSystemTimeAsFileTime.ftTimeLow, lpSystemTimeAsFileTime.ftTimeHigh)
            Try
                Dim onIntervalElapsed As ElapsedEventHandler = Me.onIntervalElapsed
                If onIntervalElapsed IsNot Nothing Then
                    If (Me.SynchronizingObject IsNot Nothing) AndAlso Me.SynchronizingObject.InvokeRequired Then
                        Me.SynchronizingObject.BeginInvoke(onIntervalElapsed, New Object() {Me, e})
                    Else
                        onIntervalElapsed(Me, e)
                    End If
                End If
            Catch
            End Try
        End If
    End Sub
End Class

請注意——無論你使用何種方式初始化第三個類型的Timer,必然會初始化TimerBack這個委托——此時其默認會綁定到其內部的一個私有函數中去判斷究竟用何種方式進行計時:如果SynchronizingObject不是null(這里假設也就是某個窗體的實例)的話,那么等於異步調用它的BeginInvoke方法——如果你的任務非常復雜,也就很可能導致WinForm宕掉。所以一般而言如果在WinForm使用這個Timer,我們一般還是手動初始化而不是直接拖拽控件

相比較而言,該控件還有一個最大的好處——它構造函數允許一個double類型的時間間隔,這就意味着可以傳入1.7976931348623157E+308毫秒的時間間隔,一般地,有時服務器定時完成的任務可能是一段很久的時間(比如一個月啥的)。因此這個類當之無愧適用於服務器,是其最佳優選對象。


免責聲明!

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



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