最近正好做一个WEB中定期执行的程序,而.NET中有3个不同的定时器。所以正好研究研究。这3个定时器分别是:

//1.实现按用户定义的时间间隔引发事件的计时器。此计时器最宜用于 Windows 窗体应用程序中,并且必须在窗口中使用。 
System.Windows.Forms.Timer

// 2.提供以指定的时间间隔执行方法的机制。无法继承此类。 
System.Threading.Timer

//3.在应用程序中生成定期事件。 
System.Timers.Timer

这三个定时器位于不同的命名空间内,上面大概介绍了3个定时器的用途,其中第一个是只能在Windows窗体中使用的控件。在.NET1.1里面,第3个System.Timers.Timer,也是可以拖拽使用,而.NET2.0开始取消了,只能手动编写代码。而后2个没有限制制。下面通过具体的列子来看3个Timer的使用和区别,网上谈的很多,但基本都没有代码。

一 System.Windows.Forms.Timer

#region System.Windows.Forms.Timer 
public partial class Form1 : Form 

public Form1() 

InitializeComponent(); 
}

int num = 0;

private void Form_Timer_Tick(object sender, EventArgs e) 

label1.Text = (++num).ToString(); 
Thread.Sleep(3000); 
}

private void button1_Click(object sender, EventArgs e) 

Form_Timer.Start(); 
}

private void button2_Click(object sender, EventArgs e) 

Form_Timer.Stop(); 


#endregion 
上面这个是一个很简单的功能,在Form窗体上拖了一个System.Windows.Forms.Timer控件名字为Form_Timer,在属性窗中把Enable属性设置为Ture,Interval是定时器的间隔时间。双击这个控件就可以看到 Form_Timer_Tick方法。在这个方法中,我们让她不停的加一个数字并显示在窗体上,2个按钮提供了对计时器的控制功能。

执行的时候你去点击其他窗体在回来,你会发现我们的窗体失去响应了。因为我们这里使用Thread.Sleep(3000);让当前线程挂起,而UI失去相应,说明了这里执行时候采用的是单线程。也就是执行定时器的线程就是UI线程。

Timer 用于以用户定义的事件间隔触发事件。Windows 计时器是为单线程环境设计的,其中,UI 线程用于执行处理。它要求用户代码有一个可用的 UI 消息泵,而且总是在同一个线程中操作,或者将调用封送到另一个线程。

在Timer内部定义的了一个Tick事件,我们前面双击这个控件时实际是增加了一行代码

this.Form_Timer.Tick += new System.EventHandler(this.Form_Timer_Tick); 
这个应该明白,不明白的可以看我BLOG中有关委托和事件的文章。然后Windows将这个定时器与调用线程关联(UI线程)。当定时器触发时,Windows把一个定时器消息插入到线程消息队列中。调用线程执行一个消息泵提取消息,然后发送到回调方法中(这里的Form_Timer_Tick方法)。而这些都是单线程进行了,所以在执行回调方法时UI会假死。所以使用这个控件不宜执行计算受限或IO受限的代码,因为这样容易导致界面假死,而应该使用多线程调用的Timer。另外要注意的是这个控件时间精度不高,精度限定为 55 毫秒。我们把Interval设置为20ms,然后在start和stop方法中记录当前时,并计算出运行时间:

从上面图可以看到程序执行了7.8S也就是 7800ms,而间隔时间是20ms,也就是最后显示数字应该是390左右,但只有250,显然是不准确的,不过按MSDN说的55ms的精度,7800ms应该只执行了140多次或更少。不知道这里是不是理解有问题。

二 System.Timers.Timer

接下来就看下另一个Timer,我们用他来改写上面的程序

#region System.Windows.Forms.Timer 
public partial class Form1 : Form 

public Form1() 

InitializeComponent(); 
}

int num = 0; 
DateTime time1 = new DateTime(); 
DateTime time2 = new DateTime(); 
//定义Timer 
System.Timers.Timer Timers_Timer = new System.Timers.Timer();

private void button1_Click(object sender, EventArgs e) 

//手动设置Timer,开始执行 
Timers_Timer.Interval = 20; 
Timers_Timer.Enabled = true; 
Timers_Timer.Elapsed += new System.Timers.ElapsedEventHandler(Timers_Timer_Elapsed); 
time1 = DateTime.Now; 
}

void Timers_Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) 

label1.Text = Convert.ToString((++num)); //显示到lable 
Thread.Sleep(3000); 
}

private void button2_Click(object sender, EventArgs e) 

//停止执行 
Timers_Timer.Enabled = false; 
time2 = DateTime.Now; 
MessageBox.Show(Convert.ToString(time2-time1)); 


#endregion 
我们可以看到这个代码和前面使用Form.Timer的基本相同,不同的是我们是手动定义的对象,而不是通过拉控件。他也有Interval ,Enabled 等属性,作用和第一是一样的。不同的是他的事件名为Elapsed ,但是和上面的Tick一样,绑定一个委托的方法。只是这里我们是手动完成的。另外不同之处是Form.Timer我们可以用Stop和Start方法控制,而这里是通过Enable属性控制。但实际上也可以用Stop和Start方法,内部也是通过他自己的Enable来控制的。

最大的不同就是上面的代码在调试时会报错,提示你"线程间操作无效: 从不是创建控件“label1”的线程访问它。"但如果你不调试直接运行是OK的,而且运行时你去拖动窗体会发现没有出现假死。从这里我们就可以知道这里的Timer的创建线程和执行线程不是同一个线程。也就是使用了多线程。Timer的创建线程是UI线程,而执行线程是TheardPool中的线程,所以不会假死,但调试的时候会报错,因为非控件的创建线程不能操作控件。但你可以直接运行,这里是VS05做了手脚。解决办法很多,用delegate.BeginInvoke()等等。这里介绍特有的一种方法,设置Timer的SynchronizingObject属性,Timers_Timer.SynchronizingObject = label1;这样的话,我们的话,调试运行时就不会报错了,但是设置了这个属性Timer就编程单线程调用了,就基本和第一个完全一样了。

Timer 是为在多线程环境中用于辅助线程而设计的。服务器计时器可以在线程间移动来处理引发的 Elapsed 事件,这样就可以比 Windows 计时器更精确地按时引发事件。Elapsed 事件在 ThreadPool 线程上引发。如果 Elapsed 事件的处理时间比 Interval 长,在另一个 ThreadPool 线程上将会再次引发此事件。因此,事件处理程序应当是可重入的。

另外和前面不同的现象是每次加1后并没有停止3秒在显示。而是继续显示,只是速度稍慢。因为我们设置间隔为20ms,而执行时间为3s,所以会在20ms后在另一个线程中继续执行,而当前线程被挂起而已。关于计时器的精度,取消3s的挂起,发现结果和第一个基本一致。

三 System.Threading.Timer

继续用这个对象改造程序。

#region System.Windows.Forms.Timer 
public partial class Form1 : Form 

public Form1() 

InitializeComponent(); 
}

int num = 0; 
DateTime time1 = new DateTime(); 
DateTime time2 = new DateTime(); 
System.Threading.Timer Thread_Time;

private void button1_Click(object sender, EventArgs e) 

//启动 
Thread_Time = new System.Threading.Timer(Thread_Timer_Method,null,0,20); 
time1 = DateTime.Now;

}

void Thread_Timer_Method(object o) 

label1.Text = Convert.ToString((++num)); 
System.Threading.Thread.Sleep(3000); 
}

private void button2_Click(object sender, EventArgs e) 

//停止 
Thread_Time.Dispose(); 
time2 = DateTime.Now; 
MessageBox.Show(Convert.ToString(time2-time1)); 


#endregion 
用Threading.Timer时的方法,和前面就不太相同了,所以的参数全部在构造函数中进行了设置,而且可以设置启动时间。而且没有提供start和stop方法来控制计时器。而且是以一种回调方法的方式实现,而不是通过事件来实现的。他们之间还是有区别的。

我们只有销毁掉对象来停止他。当你运行时,你会发现他和前面的Timers.Timer一样,是多线程的,主要表现在不会假死,调试运行报错。但跟让你奇怪的是,我们的代码竟然无法让她停止下来。调用了Dispose方法没有用。问题在那?然后有进行了测试,修改了间隔时间为100,200,500,1000,3000,4000。这几种情况。发现当间隔为500ms以上是基本马上就停止了。而间隔时间相对执行时间越短,继续执行的时间越长。这应该是在间隔时间小于执行时间时多个线程运行造成的。因为所有的线程不是同时停止的。间隔越短,线程越多,所以执行次数越多。

最后来看下这个对象另外一个特殊的地方。

static void Main() 

Timer t = new Timer(Test,null,0,1000); 
Console.ReadLine(); 
}

public static void Test(object o) 

Console.WriteLine("nihao"); 
GC.Collect(); 

这段代码会输出什么结果呢?默认情况他只输出一次,就停止了。为什么呢?根据上面说的,当定义对象t,执行代码后,进行了强制垃圾回收,因为t在Main中没有其他引用,所以被回收掉了。但是如果我们吧编译器的”优化“项取消掉,在看看情况。程序进然一直在输出。为什么执行垃圾回收却没有被回收呢?因为这个禁用优化选项,t的声明周期被扩展到了方法结束。所以一直执行。

因为编译器默认是优化的,所以我们必须保证Timer对象一直被引用,而避免被垃圾回收。所以我们可以在编译器打开优化的情况下,在Main函数最后加上t=null保证回收前被引用,但你发现,这样是没用的。因为JIT编译器优化后会吧t=null直接删除,所以我们用t.Dispose(),就可以达到目的。在我们进行垃圾回收时,CLR发现t还有被引用,还没执行Dispose所以不会被回收。是以Threading.Timer有时候会出现运行一次就停止或者是销毁了还在运行的情况,而且和编译器优化也有关,所以使用时要注意。

最后看下MSDN的描述: 只要在使用 Timer,就必须保留对它的引用。对于任何托管对象,如果没有对 Timer 的引用,计时器会被垃圾回收。即使 Timer 仍处在活动状态,也会被回收。当不再需要计时器时,请使用 Dispose 方法释放计时器持有的资源。如果希望在计时器被释放时接收到信号,请使用接受 WaitHandle 的 Dispose(WaitHandle) 方法重载。计时器已被释放后,WaitHandle 便终止。

总结:

System.Threading.Timer 是一个简单的轻量计时器,它使用回调方法并由线程池线程提供服务。不建议将其用于 Windows 窗体,因为其回调不在用户界面线程上进行。System.Windows.Forms.Timer 是用于 Windows 窗体的更佳选择。要获取基于服务器的计时器功能,可以考虑使用 System.Timers.Timer,它可以引发事件并具有其他功能。

在《CLR Via C#》中讲多线程时有提到这3个计时器,但作者说System.Timers.Timer是对System.Threading.Timer的报装,不推荐使用,但是在我的WEB项目中的Application_Start中我还是使用的这个而不是Threading.Timer,因为使用Threading.Timer时只执行了一次就不在执行了。

对于计时器在B/S结构中的使用就复杂一些,一般我们把计时器放在Application_OnStart中,这样全局维护一个计时器,可以进行定期备份数据库,定期维护用户等操作,而且方法写作静态的,以免被垃圾回收。而不建议在一般的aspx页面中使用,因为服务器端的定时器对用户这样意义不大,完全可以使用JS代替。而且这个页面的每个请求都可能引入一个新的定时器,导致系统崩溃。另外,定时器是ASP.NET进程,IIS有关,所以对用重要的执行任务,还是建议写成服务或独立程序放在服务器上执行好了。

为什么ASP.NET的Application_OnStart中使用Threading.Timer不能循环执行还不清楚,难道是GC回收或是IIS线程的问题?

最新文章

  1. Httpster –世界各地最潮的网页设计案例聚合网站
  2. IndexOf、IndexOfAny 、Remove
  3. CentOS 7 / RHEL 7 – Open ports
  4. (python)对象的引用
  5. CSS書寫規範及CSS Hack
  6. eclipse 中使用等宽字体 inconsolata
  7. cookie有效期到了后,是由浏览器还是由系统还删除的
  8. Easyui 异步树直接所有展开
  9. redis 简易监控的几种方法
  10. POJ1741
  11. linux统配符
  12. 常用java的正则表达式
  13. centos7进入单用户模式
  14. 【window】window10永久关闭更新
  15. winform中DataGridView实现分页功能
  16. CodeForces - 748E (枚举+脑洞)
  17. Linux 关于Transparent Hugepages的介绍【转】
  18. redis过期回调以及键空间通知
  19. 对 ArrayList 进行分页.
  20. NFS服务基础

热门文章

  1. apache DBUtils 使用例子demo
  2. Dubbo学习(五) Dubbo 从下载到编译成功
  3. Embarcadero Delphi 7 Enterprise 7.0.4.453 中文版
  4. jquery 半透明遮罩效果 小结
  5. 【jQuery插件】pagepiling滚屏插件使用
  6. Eclipse中设置新创建文件的默认编码格式
  7. mysql内外连接
  8. IbatisNet连接oracle 报错
  9. 【HDU4336】Card Collector(Min-Max容斥)
  10. 胡小兔的NOIP2017游记【出成绩后更新版】