.NET 中有多少种定时器
.NET 中至少有 6 种定时器,每一种定时器都有它的用途和特点。根据定时器的应用场景,可以分为 UI 相关的定时器和 UI 无关的定时器。本文将简单介绍这 6 种定时器的基本用法和特点。
UI 定时器
.NET 中的 UI 定时器主要是 WinForm、WPF 以及 WebForm 中的定时器。分别为:
System.Windows.Forms.Timer
System.Windows.Threading.DispatcherTimer
System.Web.UI.Timer
通常情况下,WinForm、WPF 中的定时器是在 UI 线程上执行回调函数,因此可以直接访问 UI 元素。由于 WinForm、WPF 支持单线程单元模型(Single-Thread Apartment,STA),定时器间隔事件是在 UI 线程上触发,因此,不用担心线程安全问题。System.Web.UI.Timer
是通过 Javascript 定时器和服务端异步回调实现,也是单线程的。
请注意,这里说的是通常情况,后边介绍
System.Windows.Threading.DispatcherTimer
时会提到在非 UI 线程创建DispatcherTimer
时也无法直接访问 UI 元素。
System.Windows.Forms.Timer
System.Windows.Forms.Timer
针对 WinForm 应用进行了优化,是只能在 WinForm 上使用的定时器。这个定时器是针对单线程环境设计的,是在 UI 线程上处理定时任务。它要求用户代码有可用的 UI 消息泵,定时任务须在 UI 线程上运行,或者跨线程通过Invoke
或者BeginInvoke
封送(marshal)到 UI 线程上运行。其优点是使用简单,只需通过给Interval
属性赋值来设置时间间隔,并注册Tick
事件处理定时任务。其缺点是精度不高,精度为 55 毫秒,也就是Interval
赋值小于 55 时,也是 55 毫秒触发一次定时任务。
System.Windows.Threading.DispatcherTimer
System.Windows.Threading.DispatcherTimer
是 WPF 中的定时器,它是基于Dispatcher
对象的(并不是基于 UI 线程的)。DispatcherTimer
的定时任务是像其他操作一样放在Dispatcher
队列上,其执行操作时间依赖于队列中其他任务及其优先级,因此,DispatcherTimer
不保证在时间间隔发生时准确执行,只保证不会在时间间隔发生前执行。
Dispatcher
为特定线程维护工作项(操作)的优先级队列,在线程上创建Dispatcher
对象时,它成为唯一可以关联该线程的Dispatcher
对象,WPF 中,DispatcherObject
只能被与之关联的Dispatcher
对象访问,也就是非 UI 线程中无法直接访问 UI 元素(WPF 中的 UI 元素都是派生自DispatcherObject
)
此外,DispatcherTimer
不像System.Windows.Forms.Timer
那样只在 UI 线程上创建才能触发Tick
事件,它在非 UI 线程下创建也可以触发Tick
事件,此时访问 UI 元素也需要通过Invoke
或者BeginInvoke
封送(marshal)到 UI 线程上运行。其优点也是简单易用,适合在 UI 线程上执行任务或触发事件,缺点是精度不准确,可能存在延迟。
上述代码中,DispatcherTimer
是非 UI 线程中创建,定时任务中访问 UI 元素 text1,需要通过Invoke
或者BeginInvoke
封送(marshal)到 UI 线程上运行,而Console.WriteLine
则可以直接运行。
System.Web.UI.Timer
System.Web.UI.Timer
是仅适用于.NET Framework
的ASP.NET
组件。通过 Javascript 定时器和服务端异步回调实现。每次触发定时器时,只能执行一个异步回调方法,而其他的异步回调方法需要等待前一个异步回调方法执行完毕后才能执行。这样可以保证在任意时刻只有一个异步回调方法在执行,避免了多线程并发执行的问题。
UI 无关定时器
从 .NET 6 开始,UI 无关定时器有三个:
System.Threading.Timer
System.Timers.Timer
System.Threading.PeriodicTimer
(.NET 6+)
System.Threading.Timer
System.Threading.Timer
是最基础轻量的定时器,它将定期在线程池线程上执行单个回调方法。在创建定时器对象时必须指定回调方法,并且后续不能修改,同时也可以指定定时器回调开始执行的时间以及时间间隔。定时器创建后可以通过Change
方法修改回调开始执行的时间以及时间间隔。该定时器的优点是轻量,精度相对较高,与 Windows 操作系统时钟精度一致,大约 15 毫秒。但因为是基于线程池的,所以在任务执行时间较长或者线程池过载时,会出现延迟。其缺点是使用不太方便,定时器创建后无法修改回调方法。
System.Timers.Timer
System.Timers.Timer
在内部使用System.Threading.Timer
,并公开了更多的属性,如AutoReset
, Enabled
或SynchronizingObject
,这些属性允许配置回调的执行方式。此外,Tick 事件允许注册多个处理程序。因此,一个定时器可以触发多个处理程序。还可以在计时器启动后更改处理程序。与System.Threading.Timer
相似,其优点也是精度相对较高,与 Windows 操作系统时钟精度一致,大约 15 毫秒。因为默认(或者SynchronizingObject=null
时)是基于线程池的,所以在任务执行时间较长或者线程池过载时,会出现延迟。但使用要更简便一些。
本例中将SynchronizingObject
属性设置为Form
对象,因此Elapsed
的处理程序在 UI 线程上执行,可以直接修改label1.Text
,如果SynchronizingObject
属性为null
,处理程序则是在线程池线程上执行,修改label1.Text
时需要通过Invoke
或者BeginInvoke
封送(marshal)到 UI 线程上运行。
System.Threading.PeriodicTimer
System.Threading.PeriodicTimer
是 .NET 6 中引入的定时器。它能方便地使用异步方式,它没有Tick
事件,而是提供WaitForNextTickAsync
方法处理定时任务。通常是使用While
循环结合CancellationToken
一起使用。和CancellationToken
一起用的时候需要注意,如果CancellationToken
被取消的时候会抛出一个OperationCanceledException
需要考虑自己处理异常。相比之前的定时器来说,有下面几个特点:[1]
没有
callback
来绑定事件;
不会发生重入,只允许有一个消费者,不允许同一个
PeriodicTimer
在不同的地方同时WaitForNextTickAsync
,不需要自己做排他锁来实现不能重入;
异步化。之前的 timer 的 callback 都是同步的,使用新 timer 可以使用异步方法,避免了编写 Sync over Async 代码;
Dispose 之后,实例就无法使用,并且 WaitForNextTickAsync 始终返回 false。
小结
我们在开发过程中遇到的坑往往不是技术本身的坑,而是我们滥用没有掌握的技术导致的,在有多种技术方案可选的时候,通常只关注技术的优点,忽略了技术适用场景及其局限性。.NET 中几种定时器各自都有其适用场景和不足,但都不支持高精度计时。了解这些有助于我们在开发过程中选择合适定时器,避免遇到问题后被动地替换解决方案。
文章转载自:czwy
原文链接:https://www.cnblogs.com/czwy/p/17862702.html
评论