1.缘起:
假设我们的报表系统需要在每天的00:05:00统计前一天的报表数据,需要在每周一的00:30:00统计上周的报表数据,又需要在每月1日的00:30:00统计上月的报表数据。
这些报表统计任务是很常见的系统需求,对于类似这样的在指定时刻执行的定时任务,我使用ESBasic.Threading.Timers.TimingTaskManager(定时任务管理器)来处理它。
TimingTaskManager与前面讲的回调定时器CallbackTimer的区别在于,CallbackTimer是参考当前时间再延迟一段时间后执行,而TimingTaskManager管理的任务是要求在指定的具体时间点执行。
定时任务管理器的形象示意图如下:
2.适用场合:
如果你的任务满足以下条件,则可以使用TimingTaskManager来解决任务的定时执行:
(1)任务需要在每小时、每天、每周、或每月的某个固定的时间点执行。
(2)可以允许任务执行的时间点与期望的时刻存在一定的误差。
3.设计思想与实现
在介绍TimingTaskManager之前,我们要先介绍TimingTask这个类,它表示一个定时任务,正是它封装了任务的执行频率、执行的具体时间和要执行的目标方法。TimingTask的类图如下:
我们看到,ExcuteTime属性是一个ShortTime类型,指定要执行任务的具体时刻。而TimingTaskType属性决定了TimingTask执行的频率,TimingTaskType定义如下:
public enum TimingTaskType
{
[EnumDescription("每小时一次")]
PerHour,
[EnumDescription("每天一次")]
PerDay,
[EnumDescription("每周一次")]
PerWeek,
[EnumDescription("每月一次")]
PerMonth
}
要注意的是,如果TimingTaskType属性的值为PerHour,则将忽略ExcuteTime的Hour属性。
同样的,DayOfWeek属性只有在TimingTaskType属性的值为PerWeek时才有效,表示在周几执行。Day属性只有在TimingTaskType属性的值为PerMonth时才有效,表示在每月的几号执行。
在TimingTask的实现中,IsOnTime方法的实现特别要引起注意。因为我们的定时任务管理器是基于定时器Timer工作的,而定时器的扫描时间是有间隔的,所以,在某个ExcuteTime所代表的真正的执行时间点的左右的两个扫描时刻,可能都会被认为是符合执行条件的(比如,两个扫描时刻距离真正执行时刻的距离都在1秒之内),如果是这样,目标任务将会被执行两次――这是我们不希望发生的。为了避免这种情况的出现,我们在TimingTask中使用lastRightTime成员来记录上次执行的时间,如果lastRightTime与当前时间的差值2倍的扫描间隔以内,则将认为当前时间不符合条件。正如下面代码所示:
TimeSpan span = now - this.lastRightTime;
if (span.TotalMilliseconds < checkSpanSeconds * 1000 * 2)
{
return false;
}
#endregion
接下来,我们将注意力转移到TimingTaskManager上来。有了TimingTask的封装,TimingTaskManager所要做的事情就非常简单,其要点归结如下:
(1)TimingTaskManager使用Timer来进行定时扫描,以判断每个任务是否到了要执行的时间点。TimerSpanInSecs属性指定了扫描的时间间隔。
(2)当某个任务的执行时刻到来,TimingTaskManager会异步执行该任务,这样不会阻塞当前的foreach遍历。
(3)TimingTaskManager提供了RegisterTask和UnRegisterTask方法,用于在运行的过程中可以动态的增加或移除任务。
(4)TimingTaskManager必须对任务列表taskList进行加锁,以确保集合的线程安全。因为定时器本身就是在另外一个线程上执行Worker方法的,如果在执行Worker方法的同时,有其它线程调用RegisterTask和UnRegisterTask方法,就会导致Worker方法中的foreach遍历动作抛出异常。
4. 使用时的注意事项
(1)由于TimingTaskManager采用Timer进行定时扫描,所以,任务执行的时间点与期望的时间点的最大误差就是TimerSpanInSecs的值。由于TimerSpanInSecs能取的最小值为1秒,所以TimingTaskManager能够达到的最小误差为1秒。如果你的任务期望被更精确的执行,那么TimingTaskManager就不适合你。
(2)TimingTaskType指定的频率只能是:每小时一次、每天一次、每周一次、每月一次。但是对于一个类似你希望在每周二、四中午12:00:00执行的任务,我们可以采用变通的做法,那就是将其视为两个任务:一个在每周二的中午12:00:00执行,另一个在每周四的中午12:00:00执行。如此,我们可以使用TimingTaskManager提供的最基础的定时频率经过组合来处理更高级、更复杂的定时任务。
(3)由于ITimingTaskExcuter的ExcuteOnTime方法是在后台线程池中的某个线程上执行的,所以其抛出的任何异常都会被忽略。最好的办法是,在实现ExcuteOnTime方法是确保在其内部catch住了所有的异常。
5.扩展
定时任务管理器TimingTaskManager暂时没有任何扩展。
注: ESBasic已经开源,点击这里下载源码。
ESBasic开源前言