高仿Timer,实现了一个不可重入定时器TimerX,与前者最大区别就是,某次定时任务未完成之前,绝对不会触发下一次任务!支持相对定时和绝对定时,还有Cron定时。
Nuget包: NewLife.Core
源码:https://github.com/NewLifeX/X/blob/master/NewLife.Core/Threading/TimerX.cs
视频:https://www.bilibili.com/video/BV1K3411K7MW
视频:https://www.bilibili.com/video/BV1BW4y1H7GM
最佳实践
每天凌晨2点备份数据库
static TimerX _timer; private static void Test1() { // 保存实例,避免被回收 _timer = new TimerX(DoBackup, null, "0 0 2 * * *") { Async = true }; } private static void DoBackup(Object state) { //todo 备份数据库 XTrace.WriteLine("开始备份cube"); }
相对定时任务
指定时间之后执行任务,并且在另一个间隔之后重复执行。
/// <summary>实例化一个不可重入的定时器</summary> /// <param name="callback">委托</param> /// <param name="state">用户数据</param> /// <param name="dueTime">多久之后开始。毫秒</param> /// <param name="period">间隔周期。毫秒</param> /// <param name="scheduler">调度器</param> public TimerX(WaitCallback callback, Object? state, Int32 dueTime, Int32 period, String? scheduler = null); new TimerX(RemoveNotAlive, null, 10 * 1000, 60 * 1000); /// <summary>移除过期的缓存项</summary> void RemoveNotAlive(Object state) { // 定时任务 }
这是最常见的用法,指定回调函数和委托参数。
第三参数dueTime是首次执行间隔;
第四参数period是以后重复执行的间隔。
!!!注意这里的间隔时间,从某一次执行完成算起间隔多少毫秒,而不是从执行开始算起。下一次什么时候执行,跟这一次执行耗时有关。
绝对定时任务
指定某个时刻执行任务,并且在该时刻之后重复执行,跟这一次执行耗时无关!
/// <summary>实例化一个绝对定时器,指定时刻执行,跟当前时间和SetNext无关</summary> /// <param name="callback">委托</param> /// <param name="state">用户数据</param> /// <param name="startTime">绝对开始时间</param> /// <param name="period">间隔周期。毫秒</param> /// <param name="scheduler">调度器</param> public TimerX(WaitCallback callback, Object state, DateTime startTime, Int32 period, String? scheduler = null);
绝对定时跟相对定时不同,不管某一次执行耗时如何,都不影响下一次执行时刻。
但如果某一次执行太长,超过了下一次执行时刻,则TimerX会忽略下一次执行。
总之,不可重入,绝不允许前后两次定时任务同时执行。
Cron定时任务
TimerX 支持使用Cron表达式创建定时任务,以满足复杂场景的需求。
/// <summary>实例化一个Cron定时器</summary> /// <param name="callback">委托</param> /// <param name="state">用户数据</param> /// <param name="cronExpression">Cron表达式</param> /// <param name="scheduler">调度器</param> public TimerX(TimerCallback callback, Object state, String cronExpression, String? scheduler = null) /// <summary>实例化一个Cron定时器</summary> /// <param name="callback">委托</param> /// <param name="state">用户数据</param> /// <param name="cronExpression">Cron表达式</param> /// <param name="scheduler">调度器</param> public TimerX(Func<Object, Task> callback, Object state, String cronExpression, String? scheduler = null)
实例化TimerX时,cronExpression 参数指定Cron表达式,例如:
// 注意,timer对象要保存起来,避免被GC回收导致定时器停止 var timer = new TimerX(DoAsyncTest2, "CronTest", "1/2 * * * *");
Cron表达式采用“秒+分+时+天+月+星期+年”的结构,能够满足大多数场景使用。需要特别指出的是,cron表达式在这里不支持“本月第几个星期几”等高级功能,建议通过普通表达式配合代码去解决。
指定调度器
构造函数最后一个参数scheduler是调度器名称,默认为空使用Default调度器。每个调度器就是一个线程Thread,挂在其上的多个TimerX实例,实质上由该线程跑时间轮TimeWheel算法。
因此,某个定时任务执行时间过长,必然影响相同调度器之下的其它定时任务。有两个解决办法:
- 指定专属调度器名称,此时该定时任务独占一个调度器,也就是独占一个线程。唯有网络协议匹配等超高精度要求时才指定调度器。
- 指定定时任务Async=true,轮到该任务执行时,采用线程池去执行,而不影响调度器线程轮转其它定时任务。强烈推荐该用法。
实际上,某一次定时任务执行超过500ms,系统也会通过日志做出友好提示。
修改下一次SetNext
有时候需要改变定时任务的下一次执行时间,而无论等待原来的时间。
SetNext参数为从现在算起的毫秒数,该时刻后执行下一次任务。很多时候使用 SetNext(-1),要求定时器尽快执行下一次。
在物联网采集的队列凑批架构中,我们常设计一个队列,并使用TimerX定时处理队列数据。待处理数据放入队列后,一般SetNext(100)唤醒定时器尽快执行,避免等太久。之所以使用100ms而不是-1,乃是因为采集线程可能多次把数据放入队列。
异步任务Async
如果定时任务执行时间比较长(超过100ms),建议设置Async=true,在任务达到执行时间时,使用线程池去执行任务,避免影响该调度器之下的其它定时任务。
当前时间TimerX.Now
在实际工作经验中,频繁调用DateTime.Now会有性能瓶颈,(每秒千万级),因其频繁调用操作系统Api。于是设计了Timerx.Now,每500ms从DateTime.Now中拷贝时间。因此它的偏差在500ms以内,适用于部分场合。