很多时候,我们程序需要在后台线程定时执行一些任务,比如定时发送邮件。简单点,我们可以自己创建一个Timer对象来定时,通过定制它的回调事件来完成具体业务需求。对于比较复杂的业务要求,稳定性要求比较高,我们可以使用一些开源框架,比如Quartz.NET创建Windows Service的方式来执行定时任务。
虽然单独的Windows Service具体有稳定性较好等特点,Quartz.NET也可以满足各种复杂的业务需求。而附加于ASP.NET进程之上的后台定时线程,容易受到ASP.NET进程影响(定时回收)等,造成不稳定和不可预测的执行结果。但是,附加后ASP.NET进程之内的后台定时线程,却具有方便部署,不需要单独安装等优势。在合理控制以及业务要求可接受的前提下,轻量级的ASP.NET进程内后台定时线程还是可以有广泛的应用。
ASP.NET的进程内的定时任务实现,可以很多方式,比如:可以直接使用Timer对象;也可以使用Quartz.NET;还可以直接使用线程池注册延迟线程的方式ThreadPool.RegisterWaitForSingleObject来达到定时执行线程的目的;甚至有人还直接使用ASP.NET的Cache的定时回收原理来实现后台线程Easy Background Tasks in ASP.NET。由于Quartz.NET本身并不支持运行在Medium Trust Level,加上它的复杂性决定放弃它用在ASP.NET进程内。使用Timer对象,必须要首先对不同名称空间下的Timer对象有一定的比较和认识,选择合适的对象,可以参考我以前的博文.NET Framework中的计时器对象;还要控制好任务的运行状态和运行周期,因为这些Timer对象基本上都是可重入的,也就是当你要执行一个需要较长执行时间的任务时,当你的任务时间超过了间隔时间,只要间隔时间一到就会在另一个线程中执行同样的任务,这样就有可能造成冲突。使用直接控制线程池API的方式来测试定时任务,在我的实践中虽然没有出现大的问题,但是我一直担心的一点是,当任务稍微多一点时,会占用线程池中大量的线程。
对于这个轻量级的小框架,还需要解放开发任务的开发人员对于任务的定时时机的把握,也就是开发定时任务的开发人员可能并不关心任务是用什么方式来执行的,也不用关心它在哪里被执行,以及间隔多长时间。任务本身的接口应该是这样的:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Job { public interface IJob { void Execute(object executionState); void Error(Exception e); } }
执行任务的工作交给统一的任务执行器来完成:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace Job { public class JobExecutor { public JobExecutor(IJob job, int interval, object executionState) { this.Job = job; this.Interval = interval; this.ExecutionState = executionState; } public IJob Job { get; private set; } public int Interval { get; private set; } public object ExecutionState { get; private set; } public bool Started { get; set; } public bool IsRunning { get; private set; } private Timer timer; public void Start() { if (Started) { return; } timer = new Timer(new TimerCallback(TimerCallback), ExecutionState, Interval, Interval); Started = true; } private void TimerCallback(object state) { if (!Started || IsRunning) { return; } timer.Change(Timeout.Infinite, Timeout.Infinite); try { Job.Execute(state); } catch (Exception e) { Job.Error(e); } IsRunning = false; if (Started) { timer.Change(Interval, Interval); } } public void Stop() { timer.Dispose(); timer = null; } } } 任务执行器,用于负责配置任务的执行周期与执行方式,然后在一个统一的任务容器中执行持有这些任务实例,让它们一直保持在主线程中存活:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace Job { public class Jobs { private static readonly Jobs instance = new Jobs(); private Dictionary<string, JobExecutor> jobs = new Dictionary<string, JobExecutor>(); /// <summary> /// Gets the instance. /// </summary> /// <value>The instance.</value> public static Jobs Instance { get { return instance; } } public void AttachJob(string name, IJob job, int interval, object executionState, bool start) { lock (lockHelper) { var jobExecutor = new JobExecutor(job, interval, executionState); jobs[name] = jobExecutor; jobExecutor.Start(); } } static object lockHelper = new object(); public void Start() { lock (lockHelper) { foreach (var job in jobs.Values) { job.Start(); } } } /// <summary> /// Stops this instance. /// </summary> public void Stop() { lock (jobs) { foreach (JobExecutor job in jobs.Values) { job.Stop(); } } } } }
这个任务容器以单例的形式一直存活在宿主进程中,可以在ASP.NET进程中,也可以是Windows Service进程。相对于Quartz.NET来讲,它已经是一个简单的不能再简单的定时任务框架了。但是很多时候,如果你的寄宿于ASP.NET进程的话,让它执行以天,日,周,月为周期的定时任务本身就不是一个明智的选择。
以上的框架,最早是从Community Server的源码中学来的,经过了多年的修改和误会。在今天,终于又回到最简单的实现。如果你一直纠结在,既要简单,又要功能丰富,还要求最小资源的框架,那么路就不是那么好走了。