1. 缘起:
假设我们的报表系统需要在每天的 00:05:00 统计前一天的报表数据,需要在每周一的 00:30:00 统计上周的报表数据,又需要在每月 1 日的 00:30:00 统计上月的报表数据。
这些报表统计任务是很常见的系统需求,对于类似这样的在指定时刻执行的定时任务,我使用 ESBasic.Threading.Timers.TimingTaskManager (定时任务管理器)来处理它。
TimingTaskManager 与前面讲的回调定时器 CallbackTimer 的区别在于, CallbackTimer 是参考当前时间再延迟一段时间后执行,而 TimingTaskManager 管理的任务是要求在指定的具体时间点执行。
定时任务管理器的形象示意图如下:
如果你的任务满足以下条件,则可以使用 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源码可到
http://esbasic.codeplex.com/
下载。
ESBasic讨论:37677395
ESBasic开源前言