唐山网站建设

设为主页 加入收藏 繁體中文

.NET框架类库中的定时器类的使用

核心提示:不论在客户端利用程序还是服务器组件(包括窗口服务)定时器通常扮演1个重要的角色。写1个高效的定时器驱动型可治理代码要求对程序流程有1个清楚的理解及掌控.NET线程模型的精巧的地方。.NET框架类库提供了3种不同的定时器类:System.Windows.Forms.Timer, System.Timer

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

Microsoft® Windows®里的定时器对象当行动产生时答应你进行控制。定时器1些最常常使用的地方就是有规律的定时启动1个进程,在事件之间设置间隔,及当进行 图形工作时保护固定的动画速度(而不管处理函数的速度)。在过往,对使用Visual Basic®的开发者来讲,定时器乃至用来摹拟多任务。

正如你所期看的那样,对你需要应对的不同场合微软为你设备了1些工具。在.NET框架类库中有3种不同的定时器类:System.Windows.Forms.Timer,System.Timers.Timer,和System.Threading.Timer。头两个类出现在Visual Studio® .NET的工具箱窗口,这两个定时器控件都答应你直接把它们拖拽到Windows窗体设计器或组件类设计器上。假设你不谨慎,这就是麻烦的开始。

Visual Studio .NET工具箱上的Windows窗体页和组件页(见Figure 1)都有定时器控件。非常轻易的毛病地使用它们当中的1个,或更糟的是,根本意识不到它们的不同。仅当目标是Windows窗体设计器时才使用Windows窗体页上的定时器控件。这个控件将在你的窗体上放置1个Systems.Windows.Forms.Timer类的实例。像工具箱上的其它控件1样,你可让Visual Studio .NET处理其天生或你自己手动的实例和初始化这个类。

  Figure 1 定时器控件

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

  Figure 2 例子程序

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

System.Windows.Forms.Timer

假设你在找1个节拍器,你已走错了地方了。这个定时器类引发的定时器事件是同你的窗口利用程序的其余代码相同步的。这意味着正在履行的代码历来不会被这个定时器类的实例所抢占(假定你不调用Application.DoEvents)。就像1个典型窗体程序里的其它代码1样,任何驻留在1个定时器事件处理函数(指的是该类型的定时器类)中的代码也是使用利用程序的UI线程所履行。在空闲时候,该UI线程一样要对利用程序的窗体消息队列中的所有消息进行负责。这不但包括由这个定时类引发的消息,也包括窗体API消息。不管甚么时候你的程序不忙于做其它事情时该UI线程就处理这些消息。

在Visual Studio .NET之前假设你写过Visual Basic代码,你可能知道在1个窗口利用程序里当正在履行1个事件处理函数时让你的UI线程往响应其它窗体消息的唯1方法就是调用Application.DoEvents方法。就像Visual Basic1样,从.NET框架中调用Application.DoEvents能够产生很多题目。Application.DoEvents产生了对UI消息泵的控制,让你对所有未处理的事件进行处理。这能够改变我刚才提到的所期看的履行路径。假设为了处理由该定时器类产生的定时器事件而在你的代码中有1个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线程睡眠时每秒钟1个。但是,当UI线程在睡眠时定时器却保持挂起状态。

对System.Windows.Forms.Timer的编程不能再简单了——它有1个非常简单和可直接编程的接口。Start和Stop方法实际上提供了1个设置使能属性的改变方法(其本身是对Win32®的SetTimer和KillTimer功能的1个包装)。我刚才提到的间隔属性,名字本身就说明了题目。即使技术上你可以设置间隔属性低到1毫秒,但你应当知道在.NET框架文档中指出这个属性大约精确到55毫秒(假定UI线程对处理是可用的)。

捕捉由System.Windows.Forms.Timer类实例引发的事件是通过感知1个标准的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类是1个服务器定时器,是为多线程环境进行设计和优化。该定时器类的实例能够被多个线程安全地访问。不像System.Windows.Forms.Timer,System.Timers.Timer缺省的,将在1个工作者线程上调用你的定时器事件处理函数,该工作者线程是从公共语言运行时(CLR)线程池中取得。这意味着在你的逝往的时间处理函数代码中必须遵从Win32编程的黄金规则:除创建该控件实例的线程之外,1个控件的实例历来不被任何其它的线程所访问。

System.Timers.Timer提供了1个简单的方法处理这样的窘境——暴露1个公共的SynchronizingObject属性。把该属性设置为1个窗体实例(或窗体上的1个控件)将保证你的事件处理函数代码运行在SynchronizingObject被实例化的同1个线程里。

假设你使用了Visual Studio .NET工具箱,Visual Studio .NET自动的设置SynchronizingObject属性为当前的窗体实例。首先它设定该定时器的SynchronizingObject属性使其在功能上同System.Windows.Forms.Timer类1样。对大部份功能,的确是这样。当操纵系统通知System.Timers.Timer类所答应的定时时间已过往,定时器使用SynchronizingObject.Begin.Invoke方法在1个线程上往履行事件拜托,该线程是创建SynchronizingObject的线程。事件处理函数将被阻塞直到UI线程能够处理它。但是不像System.Windows.Forms.Timer类1样,该事件终极依然能够被引发。像你在Figure 2中看到的,当UI线程不能够处理时System.Windows.Forms.Timer不会引发事件,可是当UI线程可用时System.Timers.Timer却会排队等候处理。

Figure 3是如何使用SynchronizingObject属性的例子。使用例子程序并通过选择System.Timers.Timer的radio按钮你可以分析这个类,并依照履行System.Windows.Forms.Timer类行动的一样顺序运行该类,这样就会产生Figure 4的输出结果。

正如你所看到的,它不会跳过1个跳动——即使UI线程在睡眠。在每1个事件间隔就有1个时间消失事件处理会被排队履行。由于UI线程在睡眠,所以当UI线程1旦被唤醒例子程序就会列出5个定时器事件(4到8)并能够处理处理函数。

正如我早先提到的,System.Timers.Timer类成员非常类似与System.Windows.Forms.Timer。最大的辨别就在与System.Timers.Timer类是对Win32可等待定时对象的1个包装,并在工作者线程上产生1个时间片消失事件而不是在UI线程上产生1个时间标记事件。时间片消失事件必须与1个同ElapsedEventHandler拜托像匹配的事件处理函数相连接。事件处理函数接受1个ElapsedEventArgs类型的参数。

除标准的EventArgs成员,ElapsedEventArgs类暴露了1个公共的SignalTime属性,它包括了1个精确的定时器时间片消失的时间。由于这个类支持不同线程的访问,除时间消失事件所在的线程,应当相信它的Stop方法能够被其它线程所调用。这会潜伏的导致消失事件被引发即使其Stop方法已被调用。你可以把SignalTime和Stop方法调用的时间进行比较来解决这个题目。

System.Timers.Timer也提供了AutoReset属性来决定当时间片消失事件引发后是继续进行还是只这1次。要记住在定时器开始后重设间隔属性会导致当前计数为0。比如,设置了1个5秒的间隔,在间隔被改变成10秒时3秒已过往了,那末下1个定时器事件将会在上1个定时器事件13秒后产生。

  System.Threading.Timer

第3个定时器类来自System.Threading名字空间。我愿意说这是所有定时器类中最好的1个,但这会引发误导。举1个例子,我惊奇的发现对驻留在System.Threading名字空间的这个类天生就不是线程安全的。(很明显,这不意味着它不能以线程安全的方式使用)。这个类的可编程接口同其它两个类也不1致,它稍微有点麻烦。

不像我开始描写的两个定时器类,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);

第1个参数(callback)要求1个TimerCallback的拜托,它指向1个方法,该方法具有下面的结构:

  

以下为援用的内容:

public void TimerCallback(object state);

第2个参数(state)可以为空或是包括程序规范信息的对象。在每1个定时器事件被调用时该state对象作为1个参数传递给你的定时回调函数。记住定时回调功能是在1个工作者线程上履行的,所以你必须确保访问state对象的线程安全。

第3个参数(dueTime)让你定义1个引发初始定时器事件的时间。你可指定1个0立即开始定时器或禁止定时器自动的开始,你可使用System.Threading.Timeout.Infinite常量。

第4个参数(period)让你定义1个回调函数被调用的时间间隔(毫秒)。给该参数定义1个0或Timeout.Infinite可以禁止后续的定时器事件调用。

1旦构造函数被调用,你依然可以通过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);

下面是我在例子程序中用到的开始和停止该定时器的代码:

以下为援用的内容:

//Initialize the timer to not start automatically...System.Threading.Timer tmrThreadingTimer = newSystem.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类1样的输出结果。由于TimerCallback功能也是在工作者线程上被调用,没有1个跳动被跳过(假定有工作者线程可用)。Figure 5显示了例子程序的输出结果。

不像System.Timers.Timer类,没有与SynchronizingObject相对应的属性被提供。任何要求访问UI控件的操纵都必须通过控件的Invoke或BeginInvoke方法被列集

  定时器的线程安全编程

为了最大限度的代码重用,3种不同类型的定时器事件都调用了一样的ShowTimerEventFired方法,下面就是3个定时器事件的处理函数:

以下为援用的内容:

private void tmrWindowsFormsTimer_Tick(object sender, System.EventArgse)
{
ShowTimerEventFired(DateTime.Now, GetThreadName());
}
private void tmrTimersTimer_Elapsed(object sender, System.TimersElapsedEventArgse){
 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线程上进行处理。为了这样做,我创建了1个ShowTimerEventFiredDelegate拜托调用:

  

以下为援用的内容:

private delegate void ShowTimerEventFiredDelegate (DateTime eventTime, string threadName);

  ShowTimerEventFiredDelegate答应ShowTimerEventFired方法在UI线程上调用它自己,Figure 6显示了产生这1切的代码。

通过查询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产生的定时器事件,有另外1个细微的地方你需要考虑。题目就是必须处理代码重进。假设你的定时器事件处理函数代码履行时间比你的定时器引发定时器事件的时间间隔要长,你预先又没有采取必要的措施保护避免多线程访问你的对象和变量,你就会陷进调试的窘境。看1下下面的代码片断:

以下为援用的内容:

private int tickCounter = 0;
private void tmrTimersTimer_Elapsed(object sender, System.Timers.ElapsedEventArgse)
{
 System.Threading.Interlocked.Increment(ref tickCounter);
Thread.Sleep(5000);
MessageBox.Show(tickCounter.ToString());
}

假定你的定时器间隔属性设置为1000毫秒,你或许会希奇当第1个信息框弹出时显示的值是5。这是由于在这5秒期间第1个定时器事件正在睡眠,而定时器却在不同的工作者线程上继续产生时间消失事件。因此,在第1个定时器事件处理完成之前tickCounter变量被增加了5次。留意我使用了Interlocked.Increment方法以线程安全的方式增加tickCounter变量的值。也有其它方法可以这样做,但是Interlock.Increment是为这类操纵而特别设计的。

解决这类题目的简单方法就是在你的事件处理函数代码块中暂时制止定时器,接着再答应定时器,就像下面的代码:

以下为援用的内容:

private void tmrTimersTimer_Elapsed(object sender, System.Timers.ElapsedEventArgse)
{
 tmrTimers.Enabled = false;
System.Threading.Interlocked.Increment(ref tickCounter);
Thread.Sleep(5000);
MessageBox.Show(tickCounter.ToString());
 tmrTimersTimer.Enabled = true;
}

有了这段代码,消息框就会每5秒钟显示1次,就像你所期看的那样,tickCounter的值每次只增加1。另外1些可选的原始同步对象就是Monitor或mutex往确保所有将来的事件被排队直到当前的事件处理函数履行完成。

  结论

为了快速方便的看到.NET框架中这3个定时器类的不同的地方,见Figure 7对3个类的比较。当使用定时器类时有1点你要考虑的就是是否是可使用Windows调度器往定期的运行标准的可履行程序来更简单的解决题目。

唐山网站建设www.fw8.net


TAG:程序,事件,函数,定时器,线程
评论加载中...
内容:
评论者: 验证码: