不论在客户端应用程序还是服务器组件(包括窗口服务)定时器通常扮演一个重要的角色。写一个高效的定时器驱动型可管理代码要求对程序流程有一个清晰的理解及掌握.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: UIThreadSleeping for 5000 ms...--> Timer Event 4 @ 4:09:36 PM on Thread: UIThreadSystem.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。