很多時候,我們程式需要在後台線程定時執行一些任務,比如定時發送郵件。簡單點,我們可以自己建立一個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的源碼中學來的,經過了多年的修改和誤會。在今天,終於又回到最簡單的實現。如果你一直糾結在,既要簡單,又要功能豐富,還要求最小資源的架構,那麼路就不是那麼好走了。