几个常见的定时器

蹲街弑〆低调 提交于 2020-03-07 10:54:39

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毫秒的时间间隔,一般地,有时服务器定时完成的任务可能是一段很久的时间(比如一个月啥的)。因此这个类当之无愧适用于服务器,是其最佳优选对象。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!