自定义定时组件
工作了这么久,封装过一部分Helper,也写过一些控件,但也没写过属于自己的框架,这次写的这个我觉得是一个组件而已,是一个定时组件。是一个定时器组件,有别于.NET Framework里面提供的几个Timer。首先说说背景,就发现现在手头上的工作离不开定时,定时做一个任务,什么都是定时去做什么什么,隔某段时间干某件事情,都离不开“定时”这个词。众所周知这个要用到多线程,在多篇关于多线程的文章里面有提过做一些周期性的操作时最好用Timer,当然这个Timer肯定是Threading.Timer,并不是WinForm控件里面的Timer。但我觉得在我的应用中Timer不够满足需求。
1.Timer只能在做任务与任务之间相隔一段时间的操作,如下图
但我需要的是这次任务开始的时刻到下次任务开始时刻是相隔同等的时间,又如下图
这样的情况下Timer则不能满足需求。
2.Timer的时间间隔一般是定的,但是如果要每次执行完任务要变动一下休眠的时间, 则需要调用Timer的Change方法。
3.Timer的休眠时间不是通过整形确定休眠的毫秒数,就是用一个TimeSpan来确定,对于那种到每天多少多少时刻或者到每小时的多少分执行一次的任务来说也不能够完全方便使用
对于上面这三种,鄙人对定时器封装了一下,弃用了Timer,还是用回了原有的Thread,定义了一种描述重复时间的模式字符串,计算出TimeSpan,从而调用Thread的Sleep()方法来休眠。下面展示整个组件的类图
最底下是两个关于时间计算方面的类,两个代理是两种任务方法的委托,基类BaseCycleMission是周期任务的积累,实现了ICycle接口,主要进行对任务线程的操控(开始,停止等),继承他的两个子类一个是实现上文第一点中我后来描述那种需求,一个类似于原有Timer的功能。它们各自使用不同的委托。MissionManager只是对所有周期任务的一个管理,统一去开启或暂停某一类的任务。
时间计算模块
那首先来介绍一下定义的字符串模式。现在遇到的周期是有两种模式,
- 一种是每隔多长时间要执行一次的任务,这个是最普通的周期形式,以每个5分钟为例,完整的形式是“-99--99--99 -99:05:00”,“-99”是缺省的意思,当然还有其他简写的模式;
- 另一种是没到什么什么时候执行一次任务,例如没到中午12点为例完整的形式是“ff-ff-ff 12: ff:ff”,“ff”是默认的形式,当然也可以用“FF”,这里也有其他简写的模式。
所有字符串的模式如下表所示
|
每到***时刻 |
每隔***时间 |
完整 |
ffff-ff-ff ff:ff:ff 或 |
-99--99--99 -99:-99:-99 |
日期部分 |
ffff-ff-ff 或 |
-99--99--99 |
时间部分 |
ff:ff:ff 或 |
-99:-99:-99 |
时间简写 |
ff:ff 或 |
-99:-99 |
那么时间计算模块的处理流程是,给定了相应的模式字符串,TimePointConverter借助正则表达式匹配出对应的模式,返回匹配出来的年月日时分秒各个值,得出结果之后就调用SleepTimeProvider来计算出线程所要休眠的时间。下面则展示一下两个类的部分代码
1 public class TimePointConverter 2 { 3 //其他成员 4 private int[] DateTimeFixBuilder(string timeStr) 5 { 6 int[] result = null; 7 8 string[] dtArray = timeStr.Split(); 9 string[] dateArray = dtArray[0].Split('-'); 10 string[] timeArray = dtArray[1].Split(':'); 11 12 uint year,month,date; 13 uint hour, minute, second; 14 uint.TryParse(dateArray[0], out year); 15 uint.TryParse(dateArray[1], out month); 16 uint.TryParse(dateArray[2], out date); 17 18 uint.TryParse(timeArray[0], out hour); 19 uint.TryParse(timeArray[1], out minute); 20 uint.TryParse(timeArray[2], out second); 21 22 //return InnerFixBuilder(year, month, date, hour, minute, second); 23 result = new int[] { (int)year, (int)month, (int)date, (int)hour, (int)minute, (int)second }; 24 return result; 25 } 26 //其他成员 27 }
1 public class SleepTimeProvider 2 { 3 //其他成员 4 public TimeSpan InnerFixBuilder(uint year, uint month, uint date, uint hour, uint minute, uint second) 5 { 6 uint[] uintTimeArray = new uint[6] { year, month, date, hour, minute, second }; 7 int[] intNowArray = new int[6] 8 { 9 DateTime.Now.Year,DateTime.Now.Month,DateTime.Now.Day, 10 DateTime.Now.Hour,DateTime.Now.Minute,DateTime.Now.Second 11 }; 12 int[] intTimeArray = new int[6]; 13 14 intTimeArray[0] = uintTimeArray[0] == 0 ? -DateTime.Now.Year : (int)uintTimeArray[0]; 15 for (int i = 1; i < uintTimeArray.Length; i++) 16 { 17 intTimeArray[i] = intTimeArray[i - 1] < 0 && uintTimeArray[i] == 0 ? 18 -intNowArray[i] : (int)uintTimeArray[i]; 19 } 20 DateTime goalTime = new DateTime(Math.Abs(intTimeArray[0]), 21 Math.Abs(intTimeArray[1]), 22 Math.Abs(intTimeArray[2]), 23 Math.Abs(intTimeArray[3]), 24 Math.Abs(intTimeArray[4]), 25 Math.Abs(intTimeArray[5])); 26 27 28 if (goalTime < DateTime.Now) 29 { 30 int max = -1; 31 for (int i = intTimeArray.Length - 1; i >= 0; i--) 32 { 33 if (intTimeArray[i] < 0 && i > max) 34 { 35 max = i; 36 intTimeArray[i]--; 37 } 38 intTimeArray[i] = Math.Abs(intTimeArray[i]); 39 } 40 goalTime = new DateTime(Math.Abs(intTimeArray[0]), 41 Math.Abs(intTimeArray[1]), 42 Math.Abs(intTimeArray[2]), 43 Math.Abs(intTimeArray[3]), 44 Math.Abs(intTimeArray[4]), 45 Math.Abs(intTimeArray[5])); 46 } 47 return goalTime - DateTime.Now; 48 } 49 50 //其他成员 51 }
线程调用模块
线程调用模块是任务执行的核心部分,MissionEntiy是对线程操作的封装,主要负责开始,停止,暂停等操作。Thread用的是后台线程,对线程操作时也多做几个判断。例如暂停那个操作的定义如下
1 public bool Pause() 2 { 3 if (actionThread == null) return false; 4 if (actionThread.ThreadState == (System.Threading.ThreadState.Running | ThreadState.Background) || 5 actionThread.ThreadState == (System.Threading.ThreadState.WaitSleepJoin | ThreadState.Background)) 6 { 7 actionThread.Suspend(); 8 return true; 9 } 10 return false; 11 }
CycleMission是真正的任务载体,里面都同样有对线程的操作,但是又外加了一些时间处理,最核心的是让线程的BuildMainAction方法,这个方法是计算出要休眠的时间,让线程休眠,到点时调用适当的方法委托。
1 public class BaseCycleMission:ICycleMission 2 { 3 //其他成员 4 5 protected void BuildMainAction(string normalCycle, string overTimeCycle, object overTimeDelegate, bool isSleepBefore, bool isInclude) 6 { 7 mainAction = () => 8 { 9 TimeSpan sleepTime = TimeSpan.MinValue; 10 TimeSpan overTime = TimeSpan.MinValue; 11 bool result = true; 12 TimePointConvert.CircleType type; 13 14 #region 提前休眠 15 if (isSleepBefore) 16 { 17 type = TimePointConvert.Default.PraseType(normalCycle); 18 19 if (type == TimePointConvert.CircleType.Interval) 20 sleepTime = SleepTimeProvider.Defult.InnerIntervalBuilder( 21 TimePointConvert.Default.ConvertCircle(normalCycle)); 22 else 23 sleepTime = SleepTimeProvider.Defult.InnerFixBuilder( 24 TimePointConvert.Default.ConvertCircle(normalCycle)); 25 if (sleepTime.TotalMilliseconds > 0) 26 Thread.Sleep(sleepTime); 27 } 28 #endregion 29 30 while (true) 31 { 32 #region 计算时间 33 if (isInclude) 34 { 35 36 type = TimePointConvert.Default.PraseType(normalCycle); 37 38 sleepTime = type == TimePointConvert.CircleType.Interval ? 39 SleepTimeProvider.Defult.InnerIntervalBuilder( 40 TimePointConvert.Default.ConvertCircle(normalCycle)) : 41 SleepTimeProvider.Defult.InnerFixBuilder( 42 TimePointConvert.Default.ConvertCircle(normalCycle)); 43 44 45 type = TimePointConvert.Default.PraseType(overTimeCycle); 46 47 overTime = type == TimePointConvert.CircleType.Interval ? 48 SleepTimeProvider.Defult.InnerIntervalBuilder( 49 TimePointConvert.Default.ConvertCircle(overTimeCycle)) : 50 SleepTimeProvider.Defult.InnerFixBuilder( 51 TimePointConvert.Default.ConvertCircle(overTimeCycle)); 52 53 } 54 #endregion 55 56 #region 执行方法 57 if (overTimeDelegate is OverTimeCycleDelegate) 58 result = (overTimeDelegate as OverTimeCycleDelegate).Invoke(); 59 else 60 { 61 (overTimeDelegate as CycleDelegate).Invoke(); 62 result = true; 63 } 64 #endregion 65 66 #region 计算时间 67 if (!isInclude) 68 { 69 70 71 type = TimePointConvert.Default.PraseType(normalCycle); 72 73 74 sleepTime = type == TimePointConvert.CircleType.Interval ? 75 SleepTimeProvider.Defult.InnerIntervalBuilder( 76 TimePointConvert.Default.ConvertCircle(normalCycle)) : 77 SleepTimeProvider.Defult.InnerFixBuilder( 78 TimePointConvert.Default.ConvertCircle(normalCycle)); 79 80 type = TimePointConvert.Default.PraseType(overTimeCycle); 81 82 overTime = type == TimePointConvert.CircleType.Interval ? 83 SleepTimeProvider.Defult.InnerIntervalBuilder( 84 TimePointConvert.Default.ConvertCircle(overTimeCycle)) : 85 SleepTimeProvider.Defult.InnerFixBuilder( 86 TimePointConvert.Default.ConvertCircle(overTimeCycle)); 87 } 88 #endregion 89 90 91 if (result) 92 { 93 if (sleepTime.TotalMilliseconds > 0) 94 Thread.Sleep(sleepTime); 95 } 96 else 97 { 98 if (overTime.TotalMilliseconds > 0) 99 Thread.Sleep(overTime); 100 } 101 } 102 }; 103 } 104 105 //其他成员 106 }
当然调用不是调用这个方法,调用只是调用它两个几类ExceptCycleMission和IncludeCycleMission,分别代表任务执行的时间不包括在周期里面和包括在周期里面两种。
管理器部分
管理器主要是一个字典集,是一个ICycleMission和字符串的字典集,里面包含了对集合里面所有元素的操作:增加,删除,运行,恢复,暂停,停止。除了删除和增加,其他都包含了类似下面的方法
RunAllMission() RunAllIncludeCycleMission() RunAllExceptCycleMission() RunMissionAmong(params string[] missionNames) RunMissionExcept(params string[] missionNames)
但是这堆方法里面都调用了CallAction这个方法,
private void CallAction(IEnumerable<ICycleMission> missionCollection,Action method) { if (missionCollection == null || method == null||missionCollection.Count()==0) return; foreach (ICycleMission item in missionCollection) { method.Method.Invoke(item, null); } }
例如在RunAllExceptCycleMission()方法里面调用如下
public void RunAllExceptCycleMission() { CallAction(this.Values.Where(c => c is ExceptCycleMission), BaseCycleMission.Default.RunMission); }
做这个组件应该是小题大做,毕竟看了那么久的《MVC框架揭秘》手痒,写了这个东西觉得框架里面的结构要设计好,命名也要命名好。