高仿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以内,适用于部分场合。