内容转自:http://hi.baidu.com/wingingbob/blog/item/4f33870999b7cf8bd1581bba.html/cmtid/6e70e651d4d83016377abe58
System.Timers命名空间[2]
System.Timers命名空间提供Timer组件,它使您可以指定的间隔引发事件。依照MSDN的说法,服务器计时器是针对编写控制服务器的服务程序而制定的[3]。System.Timers命名空间只包含了三个公开类和一个公开的委托,概述如下:
ElapsedEventArgs | 类 | “为 Timer.Elapsed 事件提供数据。”也就是Timer.Elapsed事件的参数。 |
ElapsedEventHandler | 委托 | “表示将要处理 Timer 的 Elapsed 事件的方法。”将这个委托指向我们的事件处理函数。 |
Timer | 类 | “在应用程序中生成定期事件。 ”服务器计时器组件的类。 |
TimersDescriptionAttribute | 类 | “设置可视化设计器在引用事件、扩展程序或属性时可以显示的说明。”用于VS设计器的特性,与计时器功能无关。 |
此命名空间非常简洁,就是围绕Timer类的实现。有Windows计时器的认识,对于这个Timer类的使用应该不会感到为难。今天比较懒,这是MSDN的例子:
把这段代码保存为ServerTimer01.cs文件,然后用C#编译器直接编译,在命令提示符中运行,效果如下:
程序运行后会每隔2秒钟触发一次Elapsed事件,事件参数ElapsedEventArgs对象只有一个SignalTime信号时间属性(它是通过kernel32.dll的GetSystemTimeAsFileTime函数得到的文件时间转换成DateTime类型的,此API的精度固定是100纳秒)。看起来很简单,的确如此,但是有两点你必须清楚:
1) Elapsed事件和ElapsedEventHandler委托。
虽然Elapsed事件与Windows计时器的Tick事件功能相同,但是,我们知道Windows计时器是工作的UI线程中的,而服务器计时器则工作在辅助线程中,就是说,我们委托出来的OnTimedEvent方法实际上是在辅线程中执行的。正是由于Elapsed事件在ThreadPool(线程池)线程上引发,当我们在主线程上调用Stop方法或者Enabled=false停止计时后,也许会因为并发或者延时仍然引发Elapsed事件执行处理程序。这种情况通常被称为可重入性[4]。我们需要采取措施防止重入的发生。
方法一:为多个线程共享的变量提供原子操作,通过一个原子变量保证该代码不会被重复执行。
原子操作是处理多线程共享必须的手段,更多.NET中的原子操作知识,可以学习System.Threading.Interlocked类[5]。同样是来自MSDN中的例子,它利用Interlocked.CompareExchange方法来解决计时器Stop的Elapsed事件重入问题[4]。
把这段代码保存为ServerTimer02.cs文件,然后用C#编译器直接编译,在命令提示符中运行,效果如下:
方法二:同步访问对象机制。
System.Threading.Monitor类提供的同步对象锁机制,当一个线程拥有对象的锁时,其他任何线程都不能获取该锁[6]。你知道.NET框架2.0增加了lock语句,事实上,它就是Monitor类应用的一种简化方式,好吧,大家习惯叫这类语法为.NET语法糖。到目前为止,我们从计时器的问题讨论到.NET多线程同步的问题,似乎有些跑题。MSDN中没有举出利用同步锁解决服务器计时器的Stop同步问题的例子,关于多线程同步的问题有机会在其他日志中专门讨论。
2) 何时使用KeepAlive方法。
在[例1]的注释中有讲到,当计时器被声明在一个较长执行的方法中时,需要在该方法的结束位置加入GC.KeepAlive[7]方法,使计时器不会因为失去引用而导致垃圾回收器将它提前回收。因此建议大家在使用服务器计时器时将它的声明放到类级别中。
System.Timers.Timer类
服务器计时器的成员比Windows计时器的成员多几个,它不仅有设置时间间隔的Interval属性,开始/关闭计时的Enabled属性和Start/Stop方法,处理周期事件由Windows计时器的Tick改为Elapsed,它还有另外两个重要属性和一些方法[8]。
AutoReset属性
该属性设置计时器是否在引发Elapsed事件后重新计数,以便触发下一次的Elapsed事件。当它为false时,计时器只会触发一次Elapsed事件。它的默认值 为true,就如前面的几个例子,都没有设置AutoReset属性,但是它仍能够重复地触发Elapsed事件。
SynchronizingObject属性
又回到线程同步问题,不过这个很容易使用。服务器计时器继承了Component(组件)类,我们可以将它作为组件添加到VS窗体设计器的工具箱里,当我们通过工具箱将这个计时器放入Windows窗体设计器时,该属性会自动被设置为计时器的父控件。比如说我在form1里添加了一个服务器计时器,那么它的SynchronizingObject属性就为form1。这样,便采用了一种非常简便的方法避免了线程池中无法访问主线程中组件的问题。(BTW,在下一篇讨论线程计时器的时候,你会知道它是怎样实现的。)
Close方法
释放由计时器占用的资源。既然使用的多线程的工作方式,释放资源是必要的,不需要太多的解释。
Dispose方法
释放由当前计时器使用的所有资源。原因同Close方法,不做解释。
总结
这次没有分析服务器计时器的源码,但是你知道.NET框架2.0的服务器计时器是通过线程计时器实现的,所以在下一篇讨论线程计时器之后,你对服务器计时器会有更深入的理解。有兴趣可以使用Reflector[9]自己分析。本篇日志的两个例子全部摘自MSDN,它们可以很好的表达使用服务器计时器的编程要点,因此,我重新进行了注释。
注:在.NET Framework 1.0和1.1版本中的服务器计时器的实现与现在(.NET Framework 2.0, 3.0, 3.5)的版本不同,而且在1.0, 1.1 (包括1.1 SP1)版本中的服务器计时器存在诸多BUG[10]。就目前来看,毕竟大多数人都在使用2.0之后的版本,因此不对之前的版本进行讨论。