1.引言
上一节里简单介绍了Scheduled Timer,也有园友推荐quartz.net,非常感谢他们,这个星期一直在看Scheduled Timer,就继续做笔记记录下来。
将System.Timers.Timer运行间隔Interval设置时间越短,越精确。这也就是说,Timer计时器将会以间隔很短的时间一直在运行,每次运行都将触发Elapsed事件,但是每次Elapsed事件触发,并不是要触发我们的作业工作。ScheduledTime是时间调度,它将引领事件里添加的任务按需执行,接下来介绍Scheduled Timer的时间调度。
2.IScheduledItem
触发Elapsed事件时,这样让里面的方法按需执行呢,如到了9:00就行。ScheduledTime就是要让我们的工作,按照我们的意愿来执行。
Scheduled Timer的时间调度接口声明如下:
public interface IScheduledItem { /// <summary> /// 返回 要执行的任务集合 /// </summary> /// <param name="begin">上次执行的时间</param> /// <param name="end">当前执行的时间</param> /// <param name="list"></param> void AddEventsInInterval(DateTime begin, DateTime end, List<DateTime> list); /// <summary> /// 获取下次运行的时间 /// </summary> /// <param name="time">开始计时的时间</param> /// <param name="includeStartTime">是否包含开始计时的时间</param> /// <returns></returns> DateTime NextRunTime(DateTime time, bool includeStartTime); }
IScheduledItem接口只做两件事:
- 获取一段时间内有效的执行时间段集合
- 获取下一次执行的具体时间
现在有一个任务在9:00 执行,怎么才能让这个任务在9:00准时执行呢,一要定时器Timer一直在运行,没有运行的话,那肯定是不会在后台申请线程进行执行任务的;
二要Timer的Interval间隔要足够短,如果不够短,就可能跳过了9:00,到时就没有准时执行了。
接下来介绍一下接口方法:
AddEventsInInterval 方法参考上面的注释,获取结果是 List<DateTime> list,可能有人会问,为什么是一个集合呢?上次执行,到下次执行,就一个任务吧,可能是,如果线程堵塞,就可能延时。
比如,有一个任务,每个两分钟,发送一封邮件,时间间隔Interval也是设置2分钟,假设上次发送是9:00,由于各种原因,没有按照2分钟来执行,这次执行的时间是9:10,过了10分钟,
这时,是发送一次,还是说发送5次(9:02,9:04,9:06,9:08,9:10)呢?应该是5次,所以是List集合。
那这个集合是怎么算的呢,要根据需求来定,比如上面那个每隔两分钟发邮件的,可以这么算,获取上次执行时间,和当前执行时间之间的时间差,再进行和时间间隔2分钟来划分。
NextRunTime 方法,获取下次运行的时间,有两个作用,一是可以用于AddEventsInInterval方法,每次获取这个时间,再和 当前时间对比,小于当前时间者,则是有效执行时间;
二可以动态设置定时器Timer的Interrval的间隔,执行一次时,可以设置Interval,如果下次执行时间是一分钟后,我们就可以改变Interval,而用不着设置10秒了。
3.ScheduledTime
ScheduledTime 是IScheduledItem的一个实现,直接上代码:
/// <summary> /// 预定周期枚举 /// </summary> public enum EventTimeUnit { //秒 BySecond = 1, //分 ByMinute = 2, //小时 Hourly = 3, //天 Daily = 4, //周 Weekly = 5, //月 Monthly = 6 } [Serializable] public class ScheduledTime : IScheduledItem { /// <summary> /// 间隔单位 /// </summary> private EventTimeUnit _base; /// <summary> /// 具体时间 /// </summary> private TimeSpan _offset; public ScheduledTime(EventTimeUnit etBase, TimeSpan offset) { this._base = etBase; this._offset = offset; } /// <summary> /// 添加事件时间 /// </summary> /// <param name="begin"></param> /// <param name="end"></param> /// <param name="list"></param> public void AddEventsInInterval(DateTime begin, DateTime end, List<DateTime> list) { DateTime next = NextRunTime(begin, true); while (next < end) { list.Add(next); next = IncInterval(next); } } /// <summary> /// 获取下次执行时间 /// </summary> /// <param name="time"></param> /// <param name="allowExact"></param> /// <returns></returns> public DateTime NextRunTime(DateTime time, bool allowExact) { //获取 由time开始起的下次执行时间 DateTime nextRun = LastSyncForTime(time) + _offset; if (nextRun == time && allowExact) return time; else if (nextRun > time) return nextRun; else return IncInterval(nextRun); } private DateTime LastSyncForTime(DateTime time) { switch (_base) { case EventTimeUnit.BySecond: return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second); case EventTimeUnit.ByMinute: return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, 0); case EventTimeUnit.Hourly: return new DateTime(time.Year, time.Month, time.Day, time.Hour, 0, 0); case EventTimeUnit.Daily: return new DateTime(time.Year, time.Month, time.Day); case EventTimeUnit.Weekly: return (new DateTime(time.Year, time.Month, time.Day)).AddDays(-(int)time.DayOfWeek); case EventTimeUnit.Monthly: return new DateTime(time.Year, time.Month, 1); } throw new Exception("Invalid base specified for timer."); } /// <summary> /// 增加单位间隔 /// </summary> /// <param name="last"></param> /// <returns></returns> private DateTime IncInterval(DateTime last) { switch (_base) { case EventTimeUnit.BySecond: return last.AddSeconds(1); case EventTimeUnit.ByMinute: return last.AddMinutes(1); case EventTimeUnit.Hourly: return last.AddHours(1); case EventTimeUnit.Daily: return last.AddDays(1); case EventTimeUnit.Weekly: return last.AddDays(7); case EventTimeUnit.Monthly: return last.AddMonths(1); } throw new Exception("Invalid base specified for timer."); } }
接下来进行测试
[TestMethod] public void HourlyTest() { //按小时周期 IScheduledItem item = new ScheduledTime(EventTimeUnit.Hourly, TimeSpan.FromMinutes(20)); //起始时间为 2004-01-01 00:00:00,包含起始时间,下个执行时间为:2004-01-01 00:20:00 TestItem(item, new DateTime(2004, 1, 1), true, new DateTime(2004, 1, 1, 0, 20, 0)); //起始时间为 2004-01-01 00:00:00,不包含起始时间,下个执行时间为:2004-01-01 00:20:00 TestItem(item, new DateTime(2004, 1, 1), false, new DateTime(2004, 1, 1, 0, 20, 0)); //起始时间为 2004-01-01 00:20:00,包含起始时间,下个执行时间为:2004-01-01 00:20:00 TestItem(item, new DateTime(2004, 1, 1, 0, 20, 0), true, new DateTime(2004, 1, 1, 0, 20, 0)); //起始时间为 2004-01-01 00:20:00,不包含起始时间,下个执行时间为:2004-01-01 01:20:00 TestItem(item, new DateTime(2004, 1, 1, 0, 20, 0), false, new DateTime(2004, 1, 1, 1, 20, 0)); //起始时间为 2004-01-01 00:20:01,包含起始时间,下个执行时间为:2004-01-01 01:20:00 TestItem(item, new DateTime(2004, 1, 1, 0, 20, 1), true, new DateTime(2004, 1, 1, 1, 20, 0)); } private static void TestItem(IScheduledItem item, DateTime input, bool AllowExact, DateTime ExpectedOutput) { DateTime Result = item.NextRunTime(input, AllowExact); //断言 Assert.AreEqual(ExpectedOutput, Result); }
测试 为了显示方便,就没有把 每种情况,分开写,都写在一起,有注释。其它 按分钟、天、周、月的类似,就不重复了。
4.总结
Scheduled Timer的具体的时间调度,都可以实现IScheduledItem接口,大家可以根据自己的需求来实现。Scheduled Timer的时间的计算算是一个比较核心的工作,IScheduledItem设计的也比较巧妙。
作者:Qlin
出处:http://www.cnblogs.com/qqlin/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。