实用技巧:.Net框架类库中定时器类的使用(2)

日期: 2008-06-11 作者:sixth 来源:TechTarget中国

  System.Threading.Timer


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


  不像我开始描述的两个定时器类,System.Threading.Timer有四个重载构造函数,就像下面这样:


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的委托,它指向一个方法,该方法具有下面的结构:


  public void TimerCallback(object state);


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


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


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


  一旦构造函数被调用,你仍然可以通过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 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类一样的输出结果。因为TimerCallback功能也是在工作者线程上被调用,没有一个跳动被跳过(假设有工作者线程可用)。Figure 5显示了例子程序的输出结果。


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


  定时器的线程安全编程


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


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线程上进行处理。为了这样做,我创建了一个ShowTimerEventFiredDelegate委托调用:


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


  ShowTimerEventFiredDelegate允许ShowTimerEventFired方法在UI线程上调用它自己,Figure 6显示了发生这一切的代码。


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


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毫秒,你也许会奇怪当第一个信息框弹出时显示的值是5。这是因为在这5秒期间第一个定时器事件正在睡眠,而定时器却在不同的工作者线程上继续产生时间消失事件。因此,在第一个定时器事件处理完成之前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秒钟显示一次,就像你所期望的那样,tickcounter的值每次只增加1。另外一些可选的原始同步对象就是Monitor或mutex去确保所有将来的事件被排队直到当前的事件处理函数执行完成。


  结论


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

我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。

我原创,你原创,我们的内容世界才会更加精彩!

【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】

微信公众号

TechTarget微信公众号二维码

TechTarget

官方微博

TechTarget中国官方微博二维码

TechTarget中国

电子邮件地址不会被公开。 必填项已用*标注

敬请读者发表评论,本站保留删除与本文无关和不雅评论的权力。

作者

sixth
sixth

相关推荐