定时器:.NET Framework类库中的Timer类比较
原作者:Alex Calvo
原文:http://msdn.microsoft.com/zh-cn/magazine/cc164015(en-us).aspx
翻译:flyjimi
源代码下载地址:TimersinNet.exe (126KB)
源代码在线查看
概要
在客户端程序和服务器组件(包括windows服务)中,timer(定时器)通常扮演着一个重要角色。编写高效的timer驱动的托管代码,需要对程序流程和.net线程模型的精妙有清晰的理解。.NET Framework 类库提供了三个不同的timer类:System.Windows.Forms.Timer, System,Timers.Timer 和 System.Threading.Timer。每个Timer类被设计优化用于不同的场合。本文研究了这三个Timer类,帮组你理解如何及何时该使用哪个类。
目录
System.Windows.Forms.Timer
System.Timers.Timer
System.Threading.Timer
定时器的线程安全编程
处理timer事件的重入
结论
Microsoft® Windows® 中的Timer对象在行为发生时允许你进行控制。Timer最常见的一些用法是有规律的定时启动一个进程,设置事件发生的间隔,在处理图像时维持一致的动画速度(不管处理器的速度如何)。在过去,对于使用Visual Basic®的开发人员来说, Timer甚至能用来模拟多任务。
如你所想,微软.NET Framework为你提供了处理这些任务所需的工具。在.NET Framework类库中有三个不同的Timer类:System.Windows.Forms.Timer, System,Timers.Timer 和 System.Threading.Timer。前两个类出现在Visual Studio® .NET 工具箱中,你可以直接把它们拖拽到Windows窗体设计器或组件设计器。如果你不小心,这时麻烦就开始了。
Visual Studio .NET 工具箱在Windows窗体页和组件页都有一个Timer控件(见图1)。很容易就用错了,或者更糟的是没有认识到它们是不同的。仅当目标是Windows窗体设计器时,使用Windows窗体页上的Timer控件。这个控件会在你的窗体上放置一个System.Windows.Forms.Timer类的实例。正如工具箱中的其它控件,你可以让Visual Studio .NET自动生成,或者也可以自己手工实例化、初始化这个类。
图1 定时器控件
组件页上的Timer控件可以安全地用于任何类。这个控件会创建System.Timers.Timer类的实例。如果你使用Visual Studio .NET 工具箱,无论是在Windows窗体设计器,还是在组件设计器,你都可以安全的使用这个Timer。当你处理一个继承自System.ComponentModel.Component的类(例如处理Windows服务)时,Visual Studio .NET会使用组件设计器。System.Threading.Timer类不在Visual Studio .NET 工具箱中。它稍微复杂些,但也提供了更高级的控制,稍后你将在本文看到。
我们首先研究一下System.Windows.Forms.Timer和System.Timers.Timer。这两个类有非常相似的对象模型。一会儿我会探究更高级的Sytem.Threading.Timer类。图2是我在这篇文章中引用的例子程序的屏幕截图。这个程序将帮助你更清晰的认识这几个类。你可以从文章上方的链接下载完整代码,并用它做试验。
图2 例子程序
System.Windows.Forms.Timer
如果你在找一个节拍器,你就找错地方了。这个Timer类触发的定时器事件与你的Windows窗体应用程序的其余代码是同步的。这就是说,正在执行的应用程序代码永远也不会被这个Timer类的实例抢占(假定你没有调用Application.DoEvents)。就像一个典型的Windows窗体应用程序的其余代码一样,这种Timer类的Timer事件处理器中的任何代码都是使用应用程序的UI线程来执行。在空闲时间,UI线程也负责处理应用程序的Windows消息队列中的所有消息,其中包括Windows API消息,也包括这种Timer类触发的Tick事件。当应用程序没有忙于做其他事时,UI线程就处理这些消息。
如果你在Visual Studio .NET之前,写过VB代码,你可能知道在基于Windows的应用程序中,允许UI线程在执行事件处理器时响应Windows消息的唯一方法,就是调用Application.DoEvents方法。正如VB中一样,在.NET Framework中调用Application.DoEvents会导致一些问题。Application.DoEvents移交控制给UI消息泵,允许对所有未处理的事件进行处理。这会改变我刚才提到的程序执行路径。如果在你代码里调用Application.DoEvents,你的程序流程会被中断,以便处理这个Timer类的实例所触发的定时器事件。这会导致不可预料的行为,使调试变得困难。
当我们执行例子程序,这个Timer类的行为就明显了。点击例子程序的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类的Interval属性设置为1000毫秒。正如你看到的,如果在主UI线程休眠(5秒)时,timer事件处理器继续捕获timer事件,那么一旦UI线程再次被唤醒时,就应该显示5个timer事件——UI线程休眠时每秒钟一个。然而,在UI线程休眠时,timer处于挂起状态。
用System.Windows.Forms.Timer编程已经够简单了——它有一个非常简单直观的编程接口。Start和Stop方法提供了一个设置Enable属性(对Win32® SetTimer/ KillTimer 函数的轻量级封装)的替代方法。刚才提到的Interval属性,是不言自明的。尽管技术上,你可以把Interval属性设置得低到一毫秒,但你应该知道.NET Framework文档中说这个属性只能精确到大约55毫秒(假设UI线程可用于处理)。
捕获System.Windows.Forms.Timer类的实例触发的事件,是通过把Tick事件关联到标准的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 Framework文档之处System.Timers.Timer是一个基于服务器的定时器,是为多线程环境进行设计和优化的。这个Timer类的实例可以从多线程中安全的访问。不像System.Windows.Forms.Timer,System.Timers.Timer类默认会从公共语言运行时的线程池获取一个工作线程(worker thread)来调用你的timer事件处理器。这意味着你的Elapsed事件处理器中的代码必须遵守Win32编程的黄金规则:控件的实例绝不能被除实例化它的线程以外的任何其他线程访问。
System.Timers.Timer类提供了一个简单的方式处理这样的困境——它暴露了一个公有的SynchronizingObject属性。把这个属性设置成Windows窗体的一个实例(或Windows窗体上的一个控件),可以保证你的Elapsed事件处理器中的代码运行在SynchronizingObject 被实例化的同一个线程。
如果你使用Visual Studio .NET工具箱,Visual Studio .NET会自动把SynchronizingObject属性设置为当前窗体。起初可能看起来,使用有SynchronizingObject属性的这个Timer类,使其在功能上与使用System.Windows.Forms.Timer等同。对于大部分功能,确实是这样。当操作系统通知System.Timers.Timer类启用的定时时间已过,定时器使用SynchronizingObject.BeginInvoke方法在创建SynchronizingObject的底层handle的线程上执行Elapsed事件代理。事件处理器会被阻塞,直到UI线程能处理它。然而,不像 System.Windows.Forms.Timer,事件最终还是会被触发。就像你在图2看到的,当UI线程不能处理时, System.Windows.Forms.Timer不会触发事件。而System.Timers.Timer会把事件排到队列中,等待UI线程可用时进行处理。
图3显示了如何使用SynchronizingObject 属性。你可以使用例子程序分析这个类,选择 System.Timers.Timer 单选按钮,按照执行System.Windows.Forms.Timer同样的顺序执行这个类。这样做会产生图4所示输出。
图3 使用SynchronizingObject属性
System.Timers.Timer tmrTimersTimer = new System.Timers.Timer();
tmrTimersTimer.Interval = 1000;
tmrTimersTimer.Elapsed += new
ElapsedEventHandler(tmrTimersTimer_Elapsed);
tmrTimersTimer.SynchronizingObject = this; //Synchronize with
//the current form...
tmrTimersTimer.Start();
……
private void tmrTimersTimer_Elapsed(object sender,
System.Timers.ElapsedEventArgs e) {
// Do something on the UI thread (same thread the form was
// created on)...
// If we didn't set SynchronizingObject we would be on a
// worker thread...
}
图4 输出
System.Timers.Timer Started @ 5:15:01 PM
--> Timer Event 1 @ 5:15:02 PM on Thread: WorkerThread
--> Timer Event 2 @ 5:15:03 PM on Thread: WorkerThread
--> Timer Event 3 @ 5:15:04 PM on Thread: WorkerThread
Sleeping for 5000 ms...
--> Timer Event 4 @ 5:15:05 PM on Thread: WorkerThread
--> Timer Event 5 @ 5:15:06 PM on Thread: WorkerThread
--> Timer Event 6 @ 5:15:07 PM on Thread: WorkerThread
--> Timer Event 7 @ 5:15:08 PM on Thread: WorkerThread
--> Timer Event 8 @ 5:15:09 PM on Thread: WorkerThread
System.Timers.Timer Stopped @ 5:15:10 PM
正如你看到的,它不会跳过一个节拍——即使当UI线程休眠时。在每个事件间隔,一个Elapse事件处理器被放到队列准备执行。因为UI线程在休眠,当UI线程被唤醒并可处理这些事件处理器时,例子程序一次显示了所有5个timer事件(从4到8)。
我前面说过,System.Timers.Timer类成员与 System.Windows.Forms.Timer 类成员非常相似。最大的不同在于,System.Timers.Timer是Win32 waitable timer对象的一个封装,并在一个工作线程(worker thread)触发Elapsed事件,而不是在UI线程上触发一个Tick事件。Elapsed事件必须连接到一个符合ElapsedEventHandler 代理的事件处理器。这个事件处理器接受一个ElapsedEventArgs类型的参数。
除了标准的EventArgs成员,ElapsedEventArgs类暴露了一个SignalTime属性,其中包含了定时器消逝的精确时间。因为这个类支持从不同的线程访问,应该相信Stop方法可以被除使用Elapsed事件的线程之外的线程访问。这可能潜在地导致在调用Stop方法后Elapsed事件才触发。你可以通过比较SignalTime属性和调用Stop方法的时间,来处理这种情况。
System.Timers.Timer还提供了AutoReset属性,用来决定Elapsed事件是否应该持续触发,或只触发一次。记住,在定时器启动后重置Interval属性,会把当前的计数重置为0。例如,如果设置interval=5秒,3秒后修改interval=10秒,下一次事件跟上一次事件的时间间隔将会是13秒。
System.Threading.Timer
第三个Timer类来自System.Threading命名空间。我想说这是所有timer类中最好的,但这可能会产生误导。首先,我惊奇的发现这个类的实例天生就不是线程安全的,考虑到它存在与System.Threading命名空间。(显然,这并不是说它不能以线程安全的方式使用。)这个类的编程接口与另外两个timer类不一致,而且有点笨重。
不像我前面讨论的两个Timer类,System.Threading.Timer有4个重载的构造函数。像下面这样:
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代理,这个代理指向有如下签名(signature,方法的特征标)的方法:
public void TimerCallback(object state);
第二个参数state可以是null或一个包含应用程序特定信息的对象。这个state对象在每次timer事件触发时,被传递给你的timer回调函数。记住timer回调函数是在一个工作线程上执行的,所以你应该保证对state对象的访问是线程安全的。
第三个参数dueTime允许你指定首次的timer事件应该何时触发。你可以指定0,立即启动timer,或者为了阻止timer自动启动,你可以使用 System.Threading.Timeout.Infinite 常量。
第四个参数period允许你指定回调函数被调用的时间间隔。指定0或System.Threading.Timeout.Infinite将禁止后续的timer事件触发。
调用了构造函数后,你仍然可以使用Change方法修改dueTIme和period属性。这个方法有如下4个重载:
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);
这是我在例程中用来启动和停止这个timer的代码:
//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.Infinite, Timeout.Infinite);
如你希望的,选择System.Threading.Timer类执行例子程序,产生了与选择 System.Timers.Timer 类一样的结果。因为TimerCallback函数是在一个工作线程被调用的,所以没有被跳过的节拍(假定工作线程可用)。图5显示了例子程序的输出。
图5 例子程序输出
System.Threading.Timer Started @ 7:17:11 AM
--> Timer Event 1 @ 7:17:12 AM on Thread: WorkerThread
--> Timer Event 2 @ 7:17:13 AM on Thread: WorkerThread
--> Timer Event 3 @ 7:17:14 AM on Thread: WorkerThread
Sleeping for 5000 ms...
--> Timer Event 4 @ 7:17:15 AM on Thread: WorkerThread
--> Timer Event 5 @ 7:17:16 AM on Thread: WorkerThread
--> Timer Event 6 @ 7:17:17 AM on Thread: WorkerThread
--> Timer Event 7 @ 7:17:18 AM on Thread: WorkerThread
--> Timer Event 8 @ 7:17:19 AM on Thread: WorkerThread
System.Threading.Timer Stopped @ 7:17:20 AM
不像System.Timers.Timer,System.Threading.Timer没有提供与System.Timers.Timer的SynchronizingObject对应的属性。任何需要访问UI控件的操作,必须使用控件的Invoke或BeginInvoke方法来组织。
定时器的线程安全编程
为了最大限度的重用代码,例子程序从三种不同的timer事件中调用了相同的ShowTimerEventFired方法。下面是三个timer事件处理器:
private void tmrWindowsFormsTimer_Tick(object sender,
System.EventArgs e) {
ShowTimerEventFired(DateTime.Now, GetThreadName());
}
private void tmrTimersTimer_Elapsed(object sender,
System.Timers.ElapsedEventArgs e) {
ShowTimerEventFired(DateTime.Now, GetThreadName());
}
private void tmrThreadingTimer_TimerCallback(object state) {
ShowTimerEventFired(DateTime.Now, GetThreadName());
}
如你所见,ShowTimerEventFired方法接受当前时间和当前线程的名称作为参数。为了区分工作线程和UI线程,在例子程序的主入口把CurrentThread对象的Name属性设置为“UIThread”。GetThreadName辅助方法返回Thread.CurrentThread.Name的值,或者当Thread.CurrentThread.IsThreadPoolThread属性值为true时,返回“WorkerThread”。
因为System.Timers.Timer和System.Threading.Timer的timer事件运行在工作线程,这些timer事件处理器里的任何用户界面相关的代码就必须汇集到UI线程上等待处理。为了实现这点,我创建了一个名为ShowTimerEventFiredDelegate的代理:
private delegate void
ShowTimerEventFiredDelegate
(DateTime eventTime,
string threadName);
ShowTimerEventFiredDelegate允许ShowTimerEventFired方法在UI线程上回调自己。图6显示了处理这些的代码。
图6 ShowTimerEventFired
private void ShowTimerEventFired(DateTime eventTime,
string threadName) {
//InvokeRequired will be true when using
//System.Threading.Timer or System.Timers.Timer (without a
//SynchronizationObject)...
if (lstTimerEvents.InvokeRequired) {
//Marshal this call back to the UI thread (via the form
//instance)...
BeginInvoke(new
ShowTimerEventFiredDelegate(ShowTimerEventFired),
new object[] {eventTime, threadName});
}
else
lstTimerEvents.TopIndex = lstTimerEvents.Items.Add(
String.Format("—> Timer Event {0} @ {1} on Thread:
{2}",
++_tickEventCounter, eventTime.ToLongTimeString(),
threadName));
}
通过检查控件的InvokeRequired属性,很容易判断你是否能在当前线程上安全的访问一个Windows窗体控件。在这个例子中,如果ListBox的InvokeRequired属性为true,窗体的BeginInvoke方法就被用来通过ShowTimerEventFiredDelegate再次调用ShowTimerEventFired方法。这将保证ListBox的Add方法在UI线程上执行。
如你所见,当编写异步timer事件时,有很多事情你必须知道。我建议你在使用System.Timers.Timer或System.Threading.Timer之前,阅读Ian Griffith的文章《Windows窗体:使用多线程为你的基于.NET的应用程序提供一个迅速反应的用户界面》("Windows Forms: Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads"),这篇文章发表在MSDN杂志2003年2月期刊上。
处理timer事件的重入
在处理异步timer事件时,如由System.Timers.Timer和System.Threading.Timer产生的事件,还有一个细微之处你需要考虑。这个问题与代码的重入有关。如果你的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());
}
假设你的定时器的Interval属性设置为1000毫秒,你会很惊奇的发现弹出的第一个消息框显示的值是5.这是由于在第一个timer事件休眠的5秒钟内,定时器继续在不同的工作线程上产生Elapsed事件。因此,在第一个timer事件执行完之前,tickCounter变量的值被增加了5次。注意我使用了Interlocked.Increment方法以线程安全的方式增加tickCounter变量的值。也有其他的方法可以实现,但是Interlocked.Increment方法是为这种操作专门设计的。
解决这种重入问题的一个简单方法是,在timer事件处理器代码中,临时禁用定时器,然后再启用定时器,如下面的例子所示:
private void tmrTimersTimer_Elapsed(object sender,
System.Timers.ElapsedEventArgs e) {
tmrTimersTimer.Enabled = false;
System.Threading.Interlocked.Increment(ref tickCounter);
Thread.Sleep(5000);
MessageBox.Show(tickCounter.ToString());
tmrTimersTimer.Enabled = true;
}
有了这段代码,消息框就会每个5秒钟显示一次,并且像你期望的那样,tickCounter的值每次增加1.另一个选择是使用Monitor或mutex这样原始的同步对象,保证所有后来的事件都排队等候,直到当前事件处理执行完成。
结论
通过图7中三个类的比较,可以快速回顾我对.NET Framework中三个Timer类的看法。当使用定时器时,你需要考虑的一点是,是否能用Windows调度程序(或者AT命令)定时运行一个标准的可执行程序,来更简单的解决你的问题。
图7 .NET Framework类库中的Timer类
System.Windows.Forms
System.Timers
System.Threading
Timer事件运行于哪个线程?
UI 线程
UI 或 工作线程
工作线程
实例是否线程安全?
否
是
否
熟悉、直观的对象模型?
是
是
否
需要 Windows 窗体?
是
否
否
节拍器似的高质量的拍子?
否
是*
是*
Timer事件是否支持状态对象(state object)?
否
否
是
是否能指定首次 timer 事件触发的时间?
否
否
是
类是否支持继承?
是
是
否
* 取决于系统资源(例如,工作线程)的可用性。
相关文章:
Windows Forms: Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads
背景信息:
Programming the Thread Pool in the .NET Framework: Using Timers
.NET Framework Class Library
作者简介:
Alex Calvo 是一个微软认证的.NET解决方案开发者。当他没有读书、编码或沉思时,他一定是在弹吉他。你可以通过Email(acalvo@hotmail.com.)联系他。
译者小记:
这篇文章的原文刊登在MSDN杂志2004年2月期刊中,网上早有了中文版本。Flyjimi做这个重复劳动,并不是因为之前的翻译版本不够好,而是看完原文后,突然很想自己翻译过来,借以磨砺一