`
womendu
  • 浏览: 1513596 次
  • 性别: Icon_minigender_2
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

定时器:.NET框架类库中定时器类的比较

阅读更多

原著:Alex Calvo

翻译:lxhui



原文出处:MSDN Magazine February 2004(Timer...)

转自:http://www.vckbase.com/document/viewdoc/?id=1014

原代码下载: TimersinNET.exe (126KB)

本文章假定你熟悉C#

概要
  不论在客户端应用程序还是服务器组件(包括窗口服务)定时器通常扮演一个重要的角色。写一个高效的定时器驱动型可管理代码要求对程序流程有一个清晰的理解及掌握.NET线程模型的精妙之处。.NET框架类库提供了三种不同的定时器类:System.Windows.Forms.Timer, System.Timers.Timer, 和System.Threading.Timer。每个类为不同的场合进行设计和优化。本文章将研究这三个类并让你理解如何及何时应该使用哪一个类。


  Microsoft® Windows®里的定时器对象当行为发生时允许你进行控制。定时器一些最常用的地方就是有规律的定时启动一个进程,在事件之间设置间隔,及当进行 图形工作时维护固定的动画速度(而不管处理函数的速度)。在过去,对于使用Visual Basic®的开发者来说,定时器甚至用来模拟多任务。
  正如你所期望的那样,对于你需要应对的不同场合微软为你装备了一些工具。在.NET框架类库中有三种不同的定时器类:System.Windows.Forms.Timer,System.Timers.Timer,和System.Threading.Timer。头两个类出现在Visual Studio® .NET的工具箱窗口,这两个定时器控件都允许你直接把它们拖拽到Windows窗体设计器或组件类设计器上。如果你不小心,这就是麻烦的开始。
  Visual Studio .NET工具箱上的Windows窗体页和组件页(见Figure 1)都有定时器控件。非常容易的错误地使用它们当中的一个,或者更糟糕的是,根本意识不到它们的不同。仅当目标是Windows窗体设计器时才使用Windows窗体页上的定时器控件。这个控件将在你的窗体上放置一个Systems.Windows.Forms.Timer类的实例。像工具箱上的其它控件一样,你可以让Visual Studio .NET处理其生成或者你自己手动的实例和初始化这个类。

  Figure 1定时器控件

  在组件页上的定时器控件可以被安全的用在任何类中。这个控件创建了一个System.Timers.Timer类的实例。如果你正在使用Visual Studio .NET工具箱,无论是Windows窗体设计器还是组件类设计器你都可以安全的使用这个类。在Visual Studio .NET中当你设计一个派生于System.ComponentModel.Component的类时使用组件类设计器。System.Threading.Timer类不出现在Visual Studio .NET工具箱窗口上。它稍微有点复杂但提供了一个更高级别的控件,稍后你会在本文章中看到。

  Figure 2例子程序

  让我们首先研究System.Windows.Forms.Timer和System.Timers.Timer类。这两个类有着非常相似的对象模型。稍后我将探索更加高级的System.Threading.Timer类。Figure 2 是我将在整个文章引用的例子程序的一个屏幕快照。这个应用程序将会让你获得对这几个定时器类的清晰的理解。你可以从本文章的开始链接处下载完整的代码并试验它。

System.Windows.Forms.Timer
  如果你在找一个节拍器,你已经走错了地方了。这个定时器类引发的定时器事件是同你的窗口应用程序的其余代码相同步的。这意味着正在执行的代码从来不会被这个定时器类的实例所抢占(假设你不调用Application.DoEvents)。就像一个典型窗体程序里的其它代码一样,任何驻留在一个定时器事件处理函数(指的是该类型的定时器类)中的代码也是使用应用程序的UI线程所执行。在空闲时候,该UI线程同样要对应用程序的窗体消息队列中的所有消息进行负责。这不仅包括由这个定时类引发的消息,也包括窗体API消息。无论何时你的程序不忙于做其它事情时该UI线程就处理这些消息。
  在Visual Studio .NET之前如果你写过Visual Basic代码,你可能知道在一个窗口应用程序里当正在执行一个事件处理函数时让你的UI线程去响应其它窗体消息的唯一方法就是调用Application.DoEvents方法。就像Visual Basic一样,从.NET框架中调用Application.DoEvents能够产生许多问题。Application.DoEvents产生了对UI消息泵的控制,让你对所有未处理的事件进行处理。这能够改变我刚才提到的所期望的执行路径。如果为了处理由该定时器类产生的定时器事件而在你的代码中有一个Application.DoEvents的调用,你的程序流程可能会被打断。这会产生不希望的行为并使调试困难。
  运行例子程序就会使这个定时器类的行为变得清楚。单击程序的Start按钮,接着单击Sleep按钮,最后单击Stop按钮,将会产生下面的输出结果:
System.Windows.Forms.Timer Started @ 4:09:28 PM
--> Timer Event 1 @ 4:09:29 PM on Thread:UIThread
--> Timer EVENT 2 @ 4:09:30 PM on Thread: UIThread
--> Timer Event 3 @ 4:09:31 PM on Thread: UIThread
Sleeping for 5000 ms...
--> Timer Event 4 @ 4:09:36 PM on Thread: UIThread
System.Windows.Forms.Timer Stopped @ 4:09:37 PM
  例子程序设置System.Windows.Forms.Timer类的间隔属性为1000毫秒。正如你所看到的,当UI线程正在睡眠(5秒)期间如果定时器事件处理函数仍然继续捕捉定时器事件的话,当睡眠线程再次被唤醒的时候应该有5个定时器事件被显示——在UI线程睡眠时每秒钟一个。然而,当UI线程在睡眠时定时器却保持挂起状态。
  对System.Windows.Forms.Timer的编程不能再简单了——它有一个非常简单和可直接编程的接口。Start和Stop方法实际上提供了一个设置使能属性的改变方法(其本身是对Win32®的SetTimer和KillTimer功能的一个包装)。我刚才提到的间隔属性,名字本身就说明了问题。即使技术上你可以设置间隔属性低到1毫秒,但你应该知道在.NET框架文档中指出这个属性大约精确到55毫秒(假定UI线程对于处理是可用的)。
  捕捉由System.Windows.Forms.Timer类实例引发的事件是通过感知一个标准的EventHandler委托的标记事件来处理的,就像下面的代码片断所示:
System.Windows.Forms.Timer tmrWindowsFormsTimer = new
    System.Windows.Forms.Timer();
tmrWindowsFormsTimer.Interval = 1000;
tmrWindowsFormsTimer.Tick += new
    EventHandler(tmrWindowsFormsTimer_Tick);
tmrWindowsFormsTimer.Start();
...
private void tmrWindowsFormsTimer_Tick(object sender,
    System.EventArgs e){
    //Do something on the UI thread...
}
System.Timers.Timer
  .NET框架文档指出System.Timers.Timer类是一个服务器定时器,是为多线程环境进行设计和优化。该定时器类的实例能够被多个线程安全地访问。不像System.Windows.Forms.Timer,System.Timers.Timer缺省的,将在一个工作者线程上调用你的定时器事件处理函数,该工作者线程是从公共语言运行时(CLR)线程池中获得。这意味着在你的逝去的时间处理函数代码中必须遵从Win32编程的黄金规则:除了创建该控件实例的线程之外,一个控件的实例从来不被任何其它的线程所访问。
  System.Timers.Timer提供了一个简单的方法处理这样的困境——暴露一个公共的SynchronizingObject属性。把该属性设置为一个窗体实例(或者窗体上的一个控件)将保证你的事件处理函数代码运行在SynchronizingObject被实例化的同一个线程里。
  如果你使用了Visual Studio .NET工具箱,Visual Studio .NET自动的设置SynchronizingObject属性为当前的窗体实例。首先它设定该定时器的SynchronizingObject属性使其在功能上同System.Windows.Forms.Timer类一样。对于大部分功能,的确是这样。当操作系统通知System.Timers.Timer类所允许的定时时间已过去,定时器使用SynchronizingObject.Begin.Invoke方法在一个线程上去执行事件委托,该线程是创建SynchronizingObject的线程。事件处理函数将被阻塞直到UI线程能够处理它。然而不像System.Windows.Forms.Timer类一样,该事件最终仍然能够被引发。像你在Figure 2中看到的,当UI线程不能够处理时System.Windows.Forms.Timer不会引发事件,可是当UI线程可用时System.Timers.Timer却会排队等候处理。
  Figure 3是如何使用SynchronizingObject属性的例子。使用例子程序并通过选择System.Timers.Timer的radio按钮你可以分析这个类,并按照执行System.Windows.Forms.Timer类行为的同样顺序运行该类,这样就会产生Figure 4的输出结果。
  正如你所看到的,它不会跳过一个跳动——即使UI线程在睡眠。在每一个事件间隔就有一个时间消失事件处理会被排队执行。因为UI线程在睡眠,所以当UI线程一旦被唤醒例子程序就会列出5个定时器事件(4到8)并能够处理处理函数。
  正如我早先提到的,System.Timers.Timer类成员非常类似与System.Windows.Forms.Timer。最大的区别就在与System.Timers.Timer类是对Win32可等待定时对象的一个包装,并在工作者线程上产生一个时间片消失事件而不是在UI线程上产生一个时间标记事件。时间片消失事件必须与一个同ElapsedEventHandler委托像匹配的事件处理函数相连接。事件处理函数接受一个ElapsedEventArgs类型的参数。
  除了标准的EventArgs成员,ElapsedEventArgs类暴露了一个公共的SignalTime属性,它包含了一个精确的定时器时间片消失的时间。因为这个类支持不同线程的访问,除了时间消失事件所在的线程,应该相信它的Stop方法能够被其它线程所调用。这会潜在的导致消失事件被引发即使其Stop方法已经被调用。你可以把SignalTime和Stop方法调用的时间进行比较来解决这个问题。
  System.Timers.Timer也提供了AutoReset属性来决定当时间片消失事件引发后是继续进行还是只这一次。要记住在定时器开始后重设间隔属性会导致当前计数为0。比如,设置了一个5秒的间隔,在间隔被改变为10秒时3秒已经过去了,那么下一个定时器事件将会在上一个定时器事件13秒后发生。

System.Threading.Timer
  第三个定时器类来自System.Threading名字空间。我愿意说这是所有定时器类中最好的一个,但这会引起误导。举一个例子,我惊讶的发现对于驻留在System.Threading名字空间的这个类天生就不是线程安全的。(很明显,这不意味着它不能以线程安全的方式使用)。这个类的可编程接口同其它两个类也不一致,它稍微有点麻烦。
  不像我开始描述的两个定时器类,System.Threading.Timer有四个重载构造函数,就像下面这样:
public Timer(TimerCallback callback, object state, long dueTime,
	     long period);
public Timer(TimerCallback callback, object state, UInt32 dueTime,
	     UInt32 period);
public Timer(TimerCallback callback, object state, int dueTime,
	     int period);
public Timer(TimerCallback callback, object state, TimeSpan dueTime,
	     TimeSpan period);
第一个参数(callback)要求一个TimerCallback的委托,它指向一个方法,该方法具有下面的结构:
public void TimerCallback(object state);
第二个参数(state)可以为空或者是包含程序规范信息的对象。在每一个定时器事件被调用时该state对象作为一个参数传递给你的定时回调函数。记住定时回调功能是在一个工作者线程上执行的,所以你必须确保访问state对象的线程安全。
第三个参数(dueTime)让你定义一个引发初始定时器事件的时间。你可指定一个0立即开始定时器或者阻止定时器自动的开始,你可以使用System.Threading.Timeout.Infinite常量。
第四个参数(period)让你定义一个回调函数被调用的时间间隔(毫秒)。给该参数定义一个0或者Timeout.Infinite可以阻止后续的定时器事件调用。
  一旦构造函数被调用,你仍然可以通过Change方法改变dueTime和period。该方法有下面四种重载形式:
public bool Change(int dueTime, int period);
public bool Change(uint dueTime, uint period);
public bool Change(long dueTime, long period);
public bool Change(TimeSpan dueTime, TimeSpan period);
  下面是我在例子程序中用到的开始和停止该定时器的代码:
//Initialize the timer to not start automatically...
System.Threading.Timer tmrThreadingTimer = new
System.Threading.Timer(new
		       TimerCallback(tmrThreadingTimer_TimerCallback),
		       null, System.Threading.Timeout.Infinite, 1000);
//Manually start the timer...
tmrThreadingTimer.Change(0, 1000);
//Manually stop the timer...
tmrThreadingTimer.Change(Timeout.Infinte, Timeout.Infinite);
  正如你所期望的那样,通过选择System.Threading.Timer类运行例子程序会产生同你看到的System.Timers.Timer类一样的输出结果。因为TimerCallback功能也是在工作者线程上被调用,没有一个跳动被跳过(假设有工作者线程可用)。Figure 5显示了例子程序的输出结果。
  不像System.Timers.Timer类,没有与SynchronizingObject相对应的属性被提供。任何请求访问UI控件的操作都必须通过控件的Invoke或BeginInvoke方法被列集

定时器的线程安全编程
  为了最大限度的代码重用,三种不同类型的定时器事件都调用了同样的ShowTimerEventFired方法,下面就是三个定时器事件的处理函数:
private void tmrWindowsFormsTimer_Tick(object sender,
    System.EventArgs e){
    ShowTimerEventFired(DateTime.Now, GetThreadName());
}
private void tmrTimersTimer_Elapsed(object sender,
    System.TimersElapsedEventArgs e){
    ShowTimerEventFired(DateTime.Now, GetThreadName());
}
private void tmrThreadingTimer_TimerCallback(object state){
    ShowTimerEventFired(DateTime.Now, GetThreadName());
}
  正如你所看到的,ShowTimerEventFired方法采用当前时间和当前线程名字作为参数。为了区别工作者线程和UI线程,在例子程序的主入口点设置CurrentThread对象的名字属性为"UIThread"。GetThreadName帮助函数返回Thread.CurrentThread.Name值或者当Thread.CurrentThread.IsThreadPoolThread属性为真时返回"WorkerThread"。
  因为System.Timers.Timer和System.Threading.Timer的定时器事件都是在工作者线程上执行的,所以在事件处理函数中的任何用户交互代码都不是马上进行的,而是被列集等候返回到UI线程上进行处理。为了这样做,我创建了一个ShowTimerEventFiredDelegate委托调用:
private delegate void
    ShowTimerEventFiredDelegate
    (DateTime eventTime,
     string threadName);
  ShowTimerEventFiredDelegate允许ShowTimerEventFired方法在UI线程上调用它自己,Figure 6显示了发生这一切的代码。
  通过查询InvokeRequired属性可以非常容易的知道你是否从当前线程可以安全的访问Windows窗体控件。在这个例子中,如果列表框的InvokeRequired属性为真,窗体的BeginInvoke方法就可以被ShowTimerEventFired方法调用,然后再被ShowTimerEventFiredDelegate方法调用。这能够保证列表框的Add方法在UI线程上执行。
  正如你所看到的,当你编写异步定时器事件时有许多问题需要意识到。在使用System.Timers.Timer和System.Threading.Timer之前我推荐你阅读Ian Griffith的文章“Windows Forms:Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads”, 该文刊登在MSDN杂志的2003年2月份的期刊上。

处理定时器事件重入
  当和异步定时器事件打交道时,如由System.Timers.Timer和System.Threading.Timer产生的定时器事件,有另外一个细微之处你需要考虑。问题就是必须处理代码重入。如果你的定时器事件处理函数代码执行时间比你的定时器引发定时器事件的时间间隔要长,你预先又没有采取必要的措施保护防止多线程访问你的对象和变量,你就会陷入调试的困境。看一下下面的代码片断:
private int tickCounter = 0;
private void tmrTimersTimer_Elapsed(object sender,
    System.Timers.ElapsedEventArgs e){
    System.Threading.Interlocked.Increment(ref tickCounter);
    Thread.Sleep(5000);
    MessageBox.Show(tickCounter.ToString());
}
  假设你的定时器间隔属性设置为1000毫秒,你也许会奇怪当第一个信息框弹出时显示的值是5。这是因为在这5秒期间第一个定时器事件正在睡眠,而定时器却在不同的工作者线程上继续产生时间消失事件。因此,在第一个定时器事件处理完成之前tickCounter变量被增加了5次。注意我使用了Interlocked.Increment方法以线程安全的方式增加tickCounter变量的值。也有其它方法可以这样做,但是Interlock.Increment是为这种操作而特别设计的。
  解决这种问题的简单方法就是在你的事件处理函数代码块中暂时禁止定时器,接着再允许定时器,就像下面的代码:
private void tmrTimersTimer_Elapsed(object sender,
    System.Timers.ElapsedEventArgs e){
    tmrTimers.Enabled = false;
    System.Threading.Interlocked.Increment(ref tickCounter);
    Thread.Sleep(5000);
    MessageBox.Show(tickCounter.ToString());
    tmrTimersTimer.Enabled = true;
}
  有了这段代码,消息框就会每5秒钟显示一次,就像你所期望的那样,tickCounter的值每次只增加1。另外一些可选的原始同步对象就是Monitor或mutex去确保所有将来的事件被排队直到当前的事件处理函数执行完成。

结论
  为了快速方便的看到.NET框架中这三个定时器类的不同之处,见Figure 7对三个类的比较。当使用定时器类时有一点你要考虑的就是是否可以使用Windows调度器去定期的运行标准的可执行程序来更简单的解决问题。

作者简介
  Alex Calvo 是微软认证的.NET解决方案开发者。当他不进行阅读,编码或思考时,他就弹吉他。Alex Calvo 的联系地址:acalvo@hotmail.com
译者结束语
  在翻译过程中一些术语、单词或词组总是找不到合适的汉语解释, 现已经将它们列入 MTT 术语表,希望大家能给出更好的解释:
  1. Elapsed——消失,时间消失。意思就是指所设置的定时时间间隔过去了。在文中主要有Elapsed Event, Elapsed Event
  2. handler, Elapsed timer等词。
  3. toolbox——工具箱
  4. windows tab——窗体页
  5. component tab——组件页
  6. handler——处理器,实际就是所说的函数或方法
分享到:
评论

相关推荐

    .net下的多媒体定时器

    在.NET框架下,多媒体定时器(Multimedia Timer)是一种高效且精确的计时工具,尤其适合需要高精度时间间隔触发事件的应用场景。标题中的".net下的多媒体定时器"指的是使用.NET编程语言(如C#或VB.NET)实现的多媒体...

    ASP.NET Ajax框架与组件

    - **AjaxLibrary类库(客户端)**:这是ASP.NET Ajax框架的核心组成部分之一,包含了用于处理Ajax请求的JavaScript库。该库提供了一系列的方法和对象,用于处理客户端与服务器之间的异步通信。 - **ASP.NET 2.0 Ajax...

    魏峥主编 VB.net 教学课件

    VB.NET(Visual Basic .NET)是微软公司推出的.NET框架下的一种面向对象的编程语言,它继承了Visual Basic的易用性,并结合了.NET的强大功能。本教学课件由魏峥主编,旨在帮助学习者深入理解.NET程序设计与开发技术...

    [转]c#.net 2005 windows应用程序界面美化

    .NET 2005框架是微软推出的一个重要开发平台,它极大地扩展了C#语言的功能,特别是在图形用户界面(GUI)的设计和优化上。开发者可以利用.NET 2005提供的类库和API,实现界面的动态效果、响应式设计以及更复杂的用户...

    .NET开发的小游戏

    首先,.NET框架是由微软公司开发的一套全面的开发平台,它包括了运行库、类库、编译器以及各种工具,用于构建、部署和运行多种类型的应用程序。开发者在.NET上开发时,可以借助其强大的类库,如System namespace中的...

    .Net图片转显控件

    1. **.NET框架**:.NET是由微软开发的一个开发平台,它提供了大量的类库和服务,支持多种编程语言,如C#、VB.NET、F#等。.NET框架提供了一个统一的开发环境,方便开发者创建跨平台的应用程序。 2. **数据库中的图片...

    C#常用类库最新版.zip

    9. **Task Parallel Library (TPL)**:是.NET中用于并行和并发编程的库,包含Task类和Parallel类,使得开发者可以轻松利用多核处理器的优势。 10. **JSON.NET**:是一个流行的第三方库,用于处理JSON数据,包括序列...

    C#实例开发源码飞机小游戏

    开发者利用.NET框架中的类库,如System.Drawing用于图形绘制,System.Windows.Forms或Xamarin.Forms进行用户界面设计。 3. **游戏编程基础**:游戏通常包含游戏循环、碰撞检测、计分系统、动画效果等核心元素。这个...

    备忘录.NET C#

    在.NET框架中,使用C#语言开发备忘录应用程序是一项常见的任务,这涉及到构建一个用户友好的界面,用于记录、存储和提醒用户重要事项。C#语言的强大特性和.NET Framework提供的丰富类库使得这种应用的开发变得高效且...

    asp.net高级编程 ppt

    .Net框架和通用语言运行时 - **C#源代码**:被编译成中间语言(IL),然后再由JIT编译器转换为特定平台的机器码。 - **程序集**:存储在磁盘上的可执行文件,包含了IL代码、资源和元数据。 #### 二、C#编程简介 ##...

    .NET 12306订票助手

    .NET框架是微软提出的一个软件开发平台,它包含了运行库(Common Language Runtime, CLR)和类库,提供了一整套服务来支持开发和运行应用程序。它的核心优势在于跨语言的兼容性、自动内存管理以及强大的安全特性。...

    .Net开发的模仿QQ程序

    .Net框架中的System.Net命名空间提供了丰富的网络编程API,如Socket类可以用来建立TCP连接,而BinaryFormatter或protobuf等工具可用于数据的序列化和反序列化,确保消息在客户端和服务器之间的正确传输。 此外,...

    Visual C++.NET小游戏开发时尚编程百例:Visual C++.NET小游戏开发时尚编程百例

    通过.NET框架的多媒体类,开发者可以添加背景音乐、音效事件,甚至实时音频处理,为游戏增添生动的听觉效果。 此外,用户交互是游戏设计中不可或缺的部分。书中可能讲述了如何处理键盘、鼠标输入,以及如何设计直观...

    【原创】C#基于.net6.0开发的贪吃蛇游戏源码

    在本游戏中,.NET 6.0提供了丰富的类库和API,简化了开发过程,并提高了性能。 3. **贪吃蛇游戏机制**:贪吃蛇游戏的核心在于蛇的移动、食物生成、碰撞检测和得分计算。游戏中的蛇通过不断吃食物增长,而碰到自身或...

    用c#源代码制作的定时器

    由微软开发,C#支持.NET框架,提供丰富的类库和强大的功能。在这个定时器项目中,开发者可能利用了C#的事件处理机制,以及与时间相关的类如DateTime和TimeSpan。 【压缩包子文件的文件名称列表】:定时器 文件名...

    c#写的房屋中介系统

    2. .NET框架:.NET框架是C#运行的基础,提供了类库、编译器和运行时环境。在房屋中介系统中,开发者可以使用.NET中的ADO.NET来访问和操作数据库,Entity Framework进行数据模型的建立和对象关系映射,以及Windows ...

    定时关机程序 C# 。net

    .NET框架是微软的开发平台,提供了丰富的类库和组件,支持多种语言,如C#、VB.NET、F#等。开发者可以借助.NET框架构建各种类型的应用,包括桌面、Web、移动和云应用。它包含Common Language Runtime (CLR) 和 ...

    《玩转.NET Micro Framework移植-基于STM32F10x处理器》源程序.rar

    4. **驱动程序开发**:通过书中源码学习如何编写针对STM32F10x的设备驱动,比如GPIO驱动、定时器驱动、串口驱动等,以实现与硬件的通信。 5. **嵌入式系统应用开发**:涵盖基于.NET MF的事件驱动编程模型,以及如何...

    .net 天气预报程序

    .NET框架提供了丰富的类库,使得开发者可以便捷地进行Windows应用开发,如我们这里讨论的WinForm应用程序。 WinForm是.NET Framework提供的一种用于构建桌面应用程序的用户界面框架。在我们的天气预报程序中,...

    RIP 仿真 .net 协议 路由

    在这个仿真项目中,我们使用.NET框架构建了一个RIP协议的模拟器,以便于理解和学习路由工作原理。 .NET框架是微软开发的一个全面的开发平台,用于构建、部署和运行各种应用程序,包括桌面应用、Web应用和移动应用。...

Global site tag (gtag.js) - Google Analytics