不论在客户端应用程序还是服务器组件(包括窗口服务)定时器通常扮演一个重要的角色写一个高效的定时器驱动型可管理代码要求对程序流程有一个清晰的理解及掌握NET线程模型的精妙之处NET框架类库提供了三种不同的定时器类SystemWindowsFormsTimer SystemTimersTimer 和SystemThreadingTimer每个类为不同的场合进行设计和优化本文章将研究这三个类并让你理解如何及何时应该使用哪一个类 Microsoft? Windows?里的定时器对象当行为发生时允许你进行控制定时器一些最常用的地方就是有规律的定时启动一个进程在事件之间设置间隔及当进行 图形工作时维护固定的动画速度(而不管处理函数的速度)在过去对于使用Visual Basic?的开发者来说定时器甚至用来模拟多任务 正如你所期望的那样对于你需要应对的不同场合微软为你装备了一些工具在NET框架类库中有三种不同的定时器类SystemWindowsFormsTimerSystemTimersTimer和SystemThreadingTimer头两个类出现在Visual Studio? NET的工具箱窗口这两个定时器控件都允许你直接把它们拖拽到Windows窗体设计器或组件类设计器上如果你不小心这就是麻烦的开始 Visual Studio NET工具箱上的Windows窗体页和组件页(见Figure )都有定时器控件非常容易的错误地使用它们当中的一个或者更糟糕的是根本意识不到它们的不同仅当目标是Windows窗体设计器时才使用Windows窗体页上的定时器控件这个控件将在你的窗体上放置一个SystemsWindowsFormsTimer类的实例像工具箱上的其它控件一样你可以让Visual Studio NET处理其生成或者你自己手动的实例和初始化这个类 Figure 定时器控件 在组件页上的定时器控件可以被安全的用在任何类中这个控件创建了一个SystemTimersTimer类的实例如果你正在使用Visual Studio NET工具箱无论是Windows窗体设计器还是组件类设计器你都可以安全的使用这个类在Visual Studio NET中当你设计一个派生于SystemComponentModelComponent的类时使用组件类设计器SystemThreadingTimer类不出现在Visual Studio NET工具箱窗口上它稍微有点复杂但提供了一个更高级别的控件稍后你会在本文章中看到 Figure 例子程序 让我们首先研究SystemWindowsFormsTimer和SystemTimersTimer类这两个类有着非常相似的对象模型稍后我将探索更加高级的SystemThreadingTimer类Figure 是我将在整个文章引用的例子程序的一个屏幕快照这个应用程序将会让你获得对这几个定时器类的清晰的理解你可以从本文章的开始链接处下载完整的代码并试验它 SystemWindowsFormsTimer 如果你在找一个节拍器你已经走错了地方了这个定时器类引发的定时器事件是同你的窗口应用程序的其余代码相同步的这意味着正在执行的代码从来不会被这个定时器类的实例所抢占(假设你不调用ApplicationDoEvents)就像一个典型窗体程序里的其它代码一样任何驻留在一个定时器事件处理函数(指的是该类型的定时器类)中的代码也是使用应用程序的UI线程所执行在空闲时候该UI线程同样要对应用程序的窗体消息队列中的所有消息进行负责这不仅包括由这个定时类引发的消息也包括窗体API消息无论何时你的程序不忙于做其它事情时该UI线程就处理这些消息 在Visual Studio NET之前如果你写过Visual Basic代码你可能知道在一个窗口应用程序里当正在执行一个事件处理函数时让你的UI线程去响应其它窗体消息的唯一方法就是调用ApplicationDoEvents方法就像Visual Basic一样从NET框架中调用ApplicationDoEvents能够产生许多问题ApplicationDoEvents产生了对UI消息泵的控制让你对所有未处理的事件进行处理这能够改变我刚才提到的所期望的执行路径如果为了处理由该定时器类产生的定时器事件而在你的代码中有一个ApplicationDoEvents的调用你的程序流程可能会被打断这会产生不希望的行为并使调试困难 运行例子程序就会使这个定时器类的行为变得清楚单击程序的Start按钮接着单击Sleep按钮最后单击Stop按钮将会产生下面的输出结果 SystemWindowsFormsTimer Started @ :: PM> Timer Event @ :: PM on ThreadUIThread> Timer EVENT @ :: PM on Thread: UIThread> Timer Event @ :: PM on Thread: UIThreadSleeping for ms> Timer Event @ :: PM on Thread: UIThreadSystemWindowsFormsTimer Stopped @ :: PM 例子程序设置SystemWindowsFormsTimer类的间隔属性为毫秒正如你所看到的当UI线程正在睡眠(秒)期间如果定时器事件处理函数仍然继续捕捉定时器事件的话当睡眠线程再次被唤醒的时候应该有个定时器事件被显示——在UI线程睡眠时每秒钟一个然而当UI线程在睡眠时定时器却保持挂起状态 对SystemWindowsFormsTimer的编程不能再简单了——它有一个非常简单和可直接编程的接口Start和Stop方法实际上提供了一个设置使能属性的改变方法(其本身是对Win?的SetTimer和KillTimer功能的一个包装)我刚才提到的间隔属性名字本身就说明了问题即使技术上你可以设置间隔属性低到毫秒但你应该知道在NET框架文档中指出这个属性大约精确到毫秒(假定UI线程对于处理是可用的) 捕捉由SystemWindowsFormsTimer类实例引发的事件是通过感知一个标准的EventHandler委托的标记事件来处理的就像下面的代码片断所示 SystemWindowsFormsTimer tmrWindowsFormsTimer = new SystemWindowsFormsTimer();tmrWindowsFormsTimerInterval = ;tmrWindowsFormsTimerTick += new EventHandler(tmrWindowsFormsTimer_Tick);tmrWindowsFormsTimerStart();private void tmrWindowsFormsTimer_Tick(object sender SystemEventArgs e){ //Do something on the UI thread} SystemTimersTimer NET框架文档指出SystemTimersTimer类是一个服务器定时器是为多线程环境进行设计和优化该定时器类的实例能够被多个线程安全地访问不像SystemWindowsFormsTimerSystemTimersTimer缺省的将在一个工作者线程上调用你的定时器事件处理函数该工作者线程是从公共语言运行时(CLR)线程池中获得这意味着在你的逝去的时间处理函数代码中必须遵从Win编程的黄金规则除了创建该控件实例的线程之外一个控件的实例从来不被任何其它的线程所访问 SystemTimersTimer提供了一个简单的方法处理这样的困境——暴露一个公共的SynchronizingObject属性把该属性设置为一个窗体实例(或者窗体上的一个控件)将保证你的事件处理函数代码运行在SynchronizingObject被实例化的同一个线程里 如果你使用了Visual Studio NET工具箱Visual Studio NET自动的设置SynchronizingObject属性为当前的窗体实例首先它设定该定时器的SynchronizingObject属性使其在功能上同SystemWindowsFormsTimer类一样对于大部分功能的确是这样当操作系统通知SystemTimersTimer类所允许的定时时间已过去定时器使用SynchronizingObjectBeginInvoke方法在一个线程上去执行事件委托该线程是创建SynchronizingObject的线程事件处理函数将被阻塞直到UI线程能够处理它然而不像SystemWindowsFormsTimer类一样该事件最终仍然能够被引发像你在Figure 中看到的当UI线程不能够处理时SystemWindowsFormsTimer不会引发事件可是当UI线程可用时SystemTimersTimer却会排队等候处理 Figure 是如何使用SynchronizingObject属性的例子使用例子程序并通过选择SystemTimersTimer的radio按钮你可以分析这个类并按照执行SystemWindowsFormsTimer类行为的同样顺序运行该类这样就会产生Figure 的输出结果 正如你所看到的它不会跳过一个跳动——即使UI线程在睡眠在每一个事件间隔就有一个时间消失事件处理会被排队执行因为UI线程在睡眠所以当UI线程一旦被唤醒例子程序就会列出个定时器事件(到)并能够处理处理函数 正如我早先提到的SystemTimersTimer类成员非常类似与SystemWindowsFormsTimer最大的区别就在与SystemTimersTimer类是对Win可等待定时对象的一个包装并在工作者线程上产生一个时间片消失事件而不是在UI线程上产生一个时间标记事件时间片消失事件必须与一个同ElapsedEventHandler委托像匹配的事件处理函数相连接事件处理函数接受一个ElapsedEventArgs类型的参数 除了标准的EventArgs成员ElapsedEventArgs类暴露了一个公共的SignalTime属性它包含了一个精确的定时器时间片消失的时间因为这个类支持不同线程的访问除了时间消失事件所在的线程应该相信它的Stop方法能够被其它线程所调用这会潜在的导致消失事件被引发即使其Stop方法已经被调用你可以把SignalTime和Stop方法调用的时间进行比较来解决这个问题 SystemTimersTimer也提供了AutoReset属性来决定当时间片消失事件引发后是继续进行还是只这一次要记住在定时器开始后重设间隔属性会导致当前计数为 比如设置了一个秒的间隔在间隔被改变为秒时秒已经过去了那么下一个定时器事件将会在上一个定时器事件秒后发生 SystemThreadingTimer 第三个定时器类来自SystemThreading名字空间我愿意说这是所有定时器类中最好的一个但这会引起误导举一个例子我惊讶的发现对于驻留在SystemThreading名字空间的这个类天生就不是线程安全的(很明显这不意味着它不能以线程安全的方式使用)这个类的可编程接口同其它两个类也不一致它稍微有点麻烦 不像我开始描述的两个定时器类SystemThreadingTimer有四个重载构造函数就像下面这样 public Timer(TimerCallback callback object state long dueTime long period); public Timer(TimerCallback callback object state UInt dueTime UInt 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)让你定义一个引发初始定时器事件的时间你可指定一个立即开始定时器或者阻止定时器自动的开始你可以使用SystemThreadingTimeoutInfinite常量 第四个参数(period)让你定义一个回调函数被调用的时间间隔(毫秒)给该参数定义一个或者TimeoutInfinite可以阻止后续的定时器事件调用 一旦构造函数被调用你仍然可以通过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 automaticallySystemThreadingTimer tmrThreadingTimer = newSystemThreadingTimer(new TimerCallback(tmrThreadingTimer_TimerCallback) null SystemThreadingTimeoutInfinite ); //Manually start the timertmrThreadingTimerChange( ); //Manually stop the timertmrThreadingTimerChange(TimeoutInfinte TimeoutInfinite); 正如你所期望的那样通过选择SystemThreadingTimer类运行例子程序会产生同你看到的SystemTimersTimer类一样的输出结果因为TimerCallback功能也是在工作者线程上被调用没有一个跳动被跳过(假设有工作者线程可用)Figure 显示了例子程序的输出结果 不像SystemTimersTimer类没有与SynchronizingObject相对应的属性被提供任何请求访问UI控件的操作都必须通过控件的Invoke或BeginInvoke方法被列集 定时器的线程安全编程 为了最大限度的代码重用三种不同类型的定时器事件都调用了同样的ShowTimerEventFired方法下面就是三个定时器事件的处理函数 private void tmrWindowsFormsTimer_Tick(object sender SystemEventArgse) { ShowTimerEventFired(DateTimeNow GetThreadName()); } private void tmrTimersTimer_Elapsed(object sender SystemTimersElapsedEventArgse){ ShowTimerEventFired(DateTimeNow GetThreadName()); } private void tmrThreadingTimer_TimerCallback(object state){ ShowTimerEventFired(DateTimeNow GetThreadName()); } 正如你所看到的ShowTimerEventFired方法采用当前时间和当前线程名字作为参数为了区别工作者线程和UI线程在例子程序的主入口点设置CurrentThread对象的名字属性为UIThreadGetThreadName帮助函数返回ThreadCurrentThreadName值或者当ThreadCurrentThreadIsThreadPoolThread属性为真时返回WorkerThread 因为SystemTimersTimer和SystemThreadingTimer的定时器事件都是在工作者线程上执行的所以在事件处理函数中的任何用户交互代码都不是马上进行的而是被列集等候返回到UI线程上进行处理为了这样做我创建了一个ShowTimerEventFiredDelegate委托调用 private delegate void ShowTimerEventFiredDelegate (DateTime eventTime string threadName); ShowTimerEventFiredDelegate允许ShowTimerEventFired方法在UI线程上调用它自己 Figure 显示了发生这一切的代码 通过查询InvokeRequired属性可以非常容易的知道你是否从当前线程可以安全的访问Windows窗体控件在这个例子中如果列表框的InvokeRequired属性为真窗体的BeginInvoke方法就可以被ShowTimerEventFired方法调用然后再被ShowTimerEventFiredDelegate方法调用这能够保证列表框的Add方法在UI线程上执行 正如你所看到的当你编写异步定时器事件时有许多问题需要意识到在使用SystemTimersTimer和SystemThreadingTimer之前我推荐你阅读Ian Griffith的文章Windows Forms:Give Your NETbased Application a Fast and Responsive UI with Multiple Threads 该文刊登在MSDN杂志的年月份的期刊上 处理定时器事件重入 当和异步定时器事件打交道时如由SystemTimersTimer和SystemThreadingTimer产生的定时器事件有另外一个细微之处你需要考虑问题就是必须处理代码重入如果你的定时器事件处理函数代码执行时间比你的定时器引发定时器事件的时间间隔要长你预先又没有采取必要的措施保护防止多线程访问你的对象和变量你就会陷入调试的困境看一下下面的代码片断 private int tickCounter = ; private void tmrTimersTimer_Elapsed(object sender SystemTimersElapsedEventArgse) { SystemThreadingInterlockedIncrement(ref tickCounter); ThreadSleep(); MessageBoxShow(tickCounterToString()); } 假设你的定时器间隔属性设置为毫秒你也许会奇怪当第一个信息框弹出时显示的值是这是因为在这秒期间第一个定时器事件正在睡眠而定时器却在不同的工作者线程上继续产生时间消失事件因此在第一个定时器事件处理完成之前tickCounter变量被增加了次注意我使用了InterlockedIncrement方法以线程安全的方式增加tickCounter变量的值也有其它方法可以这样做但是InterlockIncrement是为这种操作而特别设计的 解决这种问题的简单方法就是在你的事件处理函数代码块中暂时禁止定时器接着再允许定时器就像下面的代码 private void tmrTimersTimer_Elapsed(object sender SystemTimersElapsedEventArgse) { tmrTimersEnabled = false; SystemThreadingInterlockedIncrement(ref tickCounter); ThreadSleep(); MessageBoxShow(tickCounterToString()); tmrTimersTimerEnabled = true; } 有了这段代码消息框就会每秒钟显示一次就像你所期望的那样tickCounter的值每次只增加另外一些可选的原始同步对象就是Monitor或mutex去确保所有将来的事件被排队直到当前的事件处理函数执行完成 结论 为了快速方便的看到NET框架中这三个定时器类的不同之处见Figure 对三个类的比较当使用定时器类时有一点你要考虑的就是是否可以使用Windows调度器去定期的运行标准的可执行程序来更简单的解决问题 |