1.缘起:
举个例子也许就能够说清楚回调定时器的用途。假设我的订单系统接收各种不同类型的订单,当订单A进来时,系统根据订单的类型和其它特征进行综合判断后,决定A订单要在2秒之后被方法M1处理;接下来收到的B订单经过同样的判断后,决定要在10秒后被方法M2处理,……。这时候就可以用回调定时器来管理这些将要被延迟一定时间再执行的任务。
当然,我们可以使用定时器或前面介绍的循环引擎来实现这样的功能,只不过我们自己需要手动管理注册的定时回调任务,并且定时检查每一个未处理订单是否已经到了处理的时刻。而回调定时器已经自动帮我们做好了这些事情,而且还是一个与具体应用无关的通用组件,我们不需要再重复实现一个特定的类来做这件事情。
回调定时器的形象示意图如下:
2.适用场合:
设计回调定时器ESBasic.Threading.Timers.ICallbackTimer的主要是为了解决类似下面的问题:
(1) 任务(回调)需要被延迟某一个时间间隔后执行。
(2)不同的任务需要被延迟的时间间隔可能不同。
(3)不同的任务需要被处理的方式可能不同。
(4)回调的延迟不需要非常精确。
回调定时器要解决的最主要问题是第一点,后面两点也是回调定时器支持的重要特性。
3.设计思想与实现
ICallbackTimer接口的定义如下:
ICallbackTimer继承了ICycleEngine接口,这说明我们可以通过Start、Stop方法来控制回调定时器的运行,并通过DetectSpanInSecs属性来设置检测任务状态的时间间隔,当然,DetectSpanInSecs设置的值最好小于最小的回调任务的延迟时间间隔。
/// ICallbackTimer 回调定时器。
/// 注意:回调任务会异步在ThreadPool的WorkerThread上执行。即使目标任务抛出异常也不会影响INotifyTimer的继续运行。
/// </summary>
public interface ICallbackTimer<T> : ICycleEngine
{
int TaskCount { get; }
/// <summary>
/// AddCallback 添加一个回调任务。目标任务会在spanInSecs后运行。仅仅运行一次。
/// </summary>
/// <param name="spanInSecs">多少秒后执行任务</param>
/// <param name="_callback">目标方法的委托</param>
/// <param name="_callbackPara">调用目标方法的参数</param>
/// <returns>新的任务编号</returns>
int AddCallback(int spanInSecs, CbGeneric<T> _callback, T _callbackPara);
/// <summary>
/// RemoveCallback 删除目标回调任务。
/// </summary>
void RemoveCallback(int taskID);
/// <summary>
/// RemoveCallbackAndAddNew 删除目标回调任务,并添加一个新的回调任务。
/// </summary>
int RemoveCallbackAndAddNew(int taskIDToRemoved, int spanInSecs, CbGeneric<T> _newCallback, T _newCallbackPara);
/// <summary>
/// GetLeftSeconds 离目标任务被回调执行还有多长时间(s)。返回0,表示任务不存在或者任务已经被执行。
/// </summary>
int GetLeftSeconds(int taskID);
/// <summary>
/// Clear 清除所有回调任务。
/// </summary>
void Clear();
}
根据上述对回调定时器的描述,我们可以借助循环引擎来实现它。你已经看到,
ICallbackTimer接口的泛型参数T,代表的是回调执行时所用到的参数的类型。而回调方法的签名必须是只接受一个T类型的参数,并且没有返回值(即如泛型委托CbGeneric<T>)。
AddCallback方法返回一个int,表示添加的任务的唯一编号,我们可以通过这个编号来查询该任务离被回调执行的时间(GetLeftSeconds方法),或者根据该编号来取消目标回调任务的执行(RemoveCallback方法)。
CallbackTimer实现了ICallbackTimer接口,其实现要注意以下几点:
(1)CallbackTimer继承自BaseCycleEngine,它借助于循环引擎来进行任务状态的循环检测。
(2)为了允许在多线程的环境中回调定时器,CallbackTimer必须对内部集合(dicTask)进行加锁控制。
(3)CallbackTimer使用CallbackTask类来将一个定时回调任务封装起来。
(4)回调任务是异步在后台的ThreadPool的WorkerThread上执行的。所以达到执行条件的多个回调任务不会相互阻塞,而是几乎同时执行的。
(5)在DoDetect方法的实现中,我们先拷贝一份任务列表,然后再对其作foreach,而不是直接this.dicTask.Values作foreach,这是因为,如果在某个回调执行时,调用了AddCallback/RemoveCallback将修改this.dicTask的内容,而此时对this.dicTask.Values的foreach还未结束,这时foreach将抛出异常。
4. 使用时的注意事项
首先,是要注意GetLeftSeconds返回的值是不精确的。因为CallbackTimer是在每次循环检测的时候(覆写基类的DoDetect方法),修改每个CallbackTask的LeftSeconds的(通过CallbackTask的SecondsPassed方法)。所以,GetLeftSeconds方法返回的只是一个大概的而非精确的值。
其次,回调的执行是“一次性的”,即注册的一个回调任务只会被执行一次,或者被取消。
再次,回调任务定时器允许回调任务执行时抛出异常,只不过该异常会被回调定时器忽略。所以,如果你要处理该异常,就应该在回调方法中捕获这个异常。
最后,由于回调任务是异步在后台线程池中运行的,所以如果同时被执行的回调的任务很多,其数量超过了后台线程池中的线程数量,此时就会导致某些回调任务将会被进一步延迟执行。