文章目录
一、引言
在开发中,会遇到并行处理的需求。
有时只需要使用task(底层是创建个线程)来处理一下就好了。而有时则在并行处理的基础上还有时间的要求,较常见的就是每隔一定时间处理一次。当然,这用task肯定可以实现,但是时间这块得自己控制,无疑增加了工作量和不确定性。
.NET提供了叫做定时器(timer,也叫计时器)的类,它在并行处理的基础上,带了时间参数的设置,可以满足这一需求。
其实本文标题与其叫C#定时器,不如叫.NET定时器好些。因为这边介绍的定时器是.NET中的东西,不只针对C#(不过调用形式是C#的),.NET平台下的其他语言也可以用。不过.NET下的语言,用C#的可能相对多一些,所以很多时候.NET的东西也被叫做C#的东西,当然这种叫法不是很规范。
二、Timers
.NET提供了两种定时器用于多线程环境:
- System.Threading.Timer,它会以固定间隔在ThreadPool线程上执行回调函数。
- System.Timers.Timer,默认情况下,它会以固定间隔在ThreadPool线程上触发一个事件。
⚠注意:
一些.NET实现下还有其它类型的定时器:
- System.Windows.Forms.Timer,从名字中就能看出来,它是WinForms的定期触发事件的组件,是为单线程环境设计的。
- System.Web.UI.Timer,这是一个ASP.NET组件,以固定间隔执行异步或同步的网页回发。
- System.Windows.Threading.DispatcherTimer,集成到Dispatcher队列中的定时器,它会按照指定的时间间隔和指定的优先级进行处理。
1. System.Threading.Timer
1.1. 简单使用
System.Threading.Timer类能以指定间隔调用委托(连续或单次)。该委托在ThreadPool线程中执行。
创建System.Threading.Timer对象时,你需要指定一个TimerCallback委托来定义回调方法、一个传递给回调函数的可选state对象、以及首次调用回调函数之前的延迟时间和连续回调调用的时间间隔。要取消一个挂起(pending)的定时器,可以调用Timer.Dispose方法。
下面代码示例创建了一个定时器,在创建一秒后首次调用委托,之后每两秒调用一次。示例中的state对象用于计算调用委托的次数。当委托被调用10次后,计时器停止。
usingSystem;usingSystem.Threading;usingSystem.Threading.Tasks;classProgram{privatestaticTimer timer;staticvoidMain(string[] args){var timerState =newTimerState{ Counter =0};
timer =newTimer(callback:newTimerCallback(TimerTask),state: timerState,dueTime:1000,period:2000);while(timerState.Counter <=10){
Task.Delay(1000).Wait();}
timer.Dispose();
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: done.");}privatestaticvoidTimerTask(object timerState){
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: starting a new callback.");var state = timerState asTimerState;
Interlocked.Increment(ref state.Counter);}classTimerState{publicint Counter;}}
1.2. 注意点
使用TimerCallback委托来指定希望Timer执行的方法。TimerCallback委托的签名如下:
voidTimerCallback(Object state)
定时器委托是在定时器被构造之后(即new了之后)指定的,且无法更改。该方法不会在创建定时器的线程上执行;它是在系统提供的ThreadPool线程上执行。
Timer类有着与系统时钟相同的分辨率。这意味着,如果周期小于系统时钟的分辨率,TimerCallback委托将按照系统时钟的分辨率定义的时间间隔执行,在Win7和Win8系统上大约是15ms。可以使用Change方法更改到期时间和周期,或禁用定时器。
⚠注意:
只要使用Timer,就必须保持对它的引用。
与其它托管对象一样,当没有对Timer的引用时,Timer也会受到GC(垃圾回收器)的影响。即使Timer仍然处于活动状态,也不会阻止它被回收。
使用的系统时钟与GetTickCount使用的时钟相同,不受timeBeginPeriod和timeEndPeriod改变的影响。
当不再需要定时器时,使用Dispose方法释放定时器所持有的资源。注意,回调函数可能在Dispose()方法重载被调用之后才发生,因为定时器队列回调函数由线程池线程执行。可以使用Dispose(waitHandle)方法重载来等待,直到所有回调完成。
该定时器执行的回调方法是可重入的(多个线程同时执行,第一个线程还没执行完,第二个线程又进去执行了),因为它是在ThreadPool线程上调用的。如果定时器间隔小于执行回调所需的时间,或者如果所有线程池线程都在使用并且多次排队,则可以在两个线程池线程上同时执行回调。
⚠注意:
System.Threading.Timer是一个简单的轻量级定时器,它使用回调方法,由线程池线程提供服务。不建议在WinForms中使用,因为它的回调函数不会在UI线程上发生。System.Windows.Forms.Timer更适用于WinForms。对于基于服务器的定时器功能,可以考虑使用System.Timers.Timer,它会引发事件并具有额外功能。
2. System.Timers.Timer
2.1. 概述
另一个用于多线程环境的定时器是System.Timers.Timer,默认情况下,它会在ThreadPool线程中引发一个事件。
当创建System.Timers.Timer对象时,可以指定引发事件的时间间隔。使用Enabled属性来指定定时器是否引发事件。如果要指定只引发一次Elapsed事件,将AutoReset设置为false。AutoReset属性的默认值为true,意味着在interval属性定义的时间间隔内会定时引发Elapsed事件。
🔺2.2. 注意点
Timer组件是一个基于服务器的定时器,它会在经过Interval属性设置的毫秒数之后引发一个Elapsed事件。使用AutoReset属性配置Timer对象,使其只引发一次或重复引发事件。通常,Timer对象声明在类层级,以便你需要它时,它就在作用域中。
// 声明在类层级大概是这个意思?// 不是定义在局部的,而是整个类的成员变量(字段)// 这样你才类中任意地方都可以去操作它classA{Timer _timer;}
然后可以处理它的Elapsed事件来进行常规处理。例如,假设你有一个服务器,它必须每周7天、每天24小时运行。你可以创建一个使用Timer对象的服务来定期检查服务器,并确保系统已启动并运行。如果系统没有响应,服务可以尝试重新启动服务器并通知管理员。
⚠注意:
该Timer类并不适用所有的.NET实现和版本,例如,.NET Standard 1.6以及更低版本。在这些情况下,你可以使用System.Threading.Timer类。
从这句话中有种System.Timers.Timer的使用优先级比System.Threading.Timer高的感觉。
该Timer类实现了IDisposable接口。当你使用完该类后,应该销毁它。要直接销毁该类,在try/catch块中调用它的dispose方法。间接销毁,可以使用using。
基于服务器的System.Timers.Timer类是为多线程环境中的工作线程而设计的。服务器定时器可以在线程之间移动来处理引发的Elapsed事件,在引发事件及时性上比Windows定时器更精确。
System.Timers.Timer组件根据Interval属性的值(以毫秒为单位)引发Elapsed事件。通过处理此事件来执行所需的处理过程。例如,假设你有一个在线销售应用程序,它不断向数据库发布销售订单。编译运输指令的服务对一批订单进行操作而不是单独处理每个订单。你可以使用Timer来每30分钟启动批处理。
⚠注意:
System.Timers.Timer类具有与系统时钟相同的分辨率。
这意味着如果Interval属性小于系统时钟分辨率,则Elapsed事件将按照系统时钟分辨率定义的时间间隔触发。
Timer组件捕获并抑制事件处理程序为Elapsed事件抛出的所有异常(也就是说,在Elapsed事件处理器中抛出的异常,你在其它线程中无法直接感受到。这就可能导致,如果你在Elapsed事件处理器中没有添加异常处理机制,并且里面抛出异常了,从线程外看好像啥也没发生)。但是要注意,对于异步执行并包含await操作符(在C#中)或await操作符(在VB中)的事件处理程序则不然。这些事件处理程序中抛出的异常会传回调用线程,如下所示:
usingSystem;usingSystem.Threading.Tasks;usingSystem.Timers;classExample{staticvoidMain(){Timer timer =newTimer(1000);
timer.Elapsed +=async( sender, e )=>awaitHandleTimer();
timer.Start();
Console.Write("Press any key to exit... ");
Console.ReadKey();}privatestaticTaskHandleTimer(){
Console.WriteLine("\nHandler not implemented...");thrownewNotImplementedException();}}// The example displays output like the following:// Press any key to exit...// Handler not implemented...// // Unhandled Exception: System.NotImplementedException: The method or operation is not implemented.// at Example.HandleTimer()// at Example.<<Main>b__0>d__2.MoveNext()// --- End of stack trace from previous location where exception was thrown ---// at System.Runtime.CompilerServices.AsyncMethodBuilderCore.<>c__DisplayClass2.<ThrowAsync>b__5(Object state)// at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)// at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)// at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()// at System.Threading.ThreadPoolWorkQueue.Dispatch()
若SynchronizingObject属性为null,则Elapsed事件在ThreadPool线程中引发。若Elapsed事件的处理耗时长于Interval,则该事件可能会在另一个ThreadPool线程上再次引发。该情况下,事件处理程序应该是可重入的(reentrant)。
⚠注意:
事件处理方法可能运行在一个线程,同时另一个线程调用Stop方法或将Enabled属性设置为false。这可能导致在定时器停止后引发Elapsed事件。Stop方法的示例代码展示了一种避免这种竞争条件的方法。
后面这部分等学了await再看。
三、总结
总的来说,System.Threading.Timer和System.Timers.Timer表面上主要异同是,
1️⃣前者是直接调用委托,而后者是引发事件。
2️⃣两者都是运行在系统线程池线程上的
3️⃣两者时钟分辨率都等于系统时钟分辨率
4️⃣前者可能较轻量级,后者是服务器级别的(但这点很模糊,我的理解是一般的桌面程序用前者即可,如果程序相对较大,可能用后者好)
5️⃣System.Timers.Timer会捕获并抑制Elapsed事件处理器抛出的所有异常。
如果你想将这两者用于WPF应用,尤其是MVVM的VM中来实现多线程改变绑定的数据,那可能会达不到预期效果,因为在WPF框架的设定下,非UI线程直接或间接访问UI线程是不合法的。如ObservableCollection之类的集合跨线程访问时,大多会报错System.NotSupportedException。
最后根据官方文档描述,一般都是推荐用后者的。
版权归原作者 MelonSuika 所有, 如有侵权,请联系我们删除。