基于 Quartz.NET 实现可中断的任务
Quartz.NET 是一个开源的作业调度框架,非常适合在平时的工作中,定时轮询数据库同步,定时邮件通知,定时处理数据等。 Quartz.NET 允许开发人员根据时间间隔(或天)来调度作业。它实现了作业和触发器的多对多关系,还能把多个作业与不同的触发器关联。整合了 Quartz.NET 的应用程序可以重用来自不同事件的作业,还可以为一个事件组合多个作业。 在 Quartz.NET 的默认实现中 Worker 并非后台线程( IsBackground= false ),所以当我们终止调度器(调用 Scheduler.Shutdown() 方法)时,假如有一个比较耗时的 Job 正在执行,那么进程将不会立即结束,而是等待这个 Job 执行完毕再结束。
为了可以立即退出进程,我们需要了解一个 Quartz.NET 中内置的接口 : IInterruptableJob 。该接口表示一个可中断的任务,实现该接口的 Job 被认为是可以被中断执行的,下面是官方对 IInterruptableJob 接口的定义和解释:
The interface to be implemented by Quartz.IJobs that provide a mechanism for having their execution interrupted. It is NOT a requirement for jobs to implement this interface - in fact, for most people, none of their jobs will. The means of actually interrupting the Job must be implemented within the Quartz.IJob itself (the Quartz.IInterruptableJob.Interrupt method of this interface is simply a means for the scheduler to inform the Quartz.IJob that a request has been made for it to be interrupted). The mechanism that your jobs use to interrupt themselves might vary between implementations. However the principle idea in any implementation should be to have the body of the job's Quartz.IJob.Execute(Quartz.IJobExecutionContext) periodically check some flag to see if an interruption has been requested, and if the flag is set, somehow abort the performance of the rest of the job's work. An example of interrupting a job can be found in the source for the class Example7's DumbInterruptableJob It is legal to use some combination of System.Threading.Monitor.Wait(System.Object) and System.Threading.Monitor.Pulse(System.Object) synchronization within System.Threading.Thread.Interrupt and Quartz.IJob.Execute(Quartz.IJobExecutionContext) in order to have the System.Threading.Thread.Interrupt method block until the Quartz.IJob.Execute(Quartz.IJobExecutionContext) signals that it has noticed the set flag. If the Job performs some form of blocking I/O or similar functions, you may want to consider having the Quartz.IJob.Execute(Quartz.IJobExecutionContext) method store a reference to the calling System.Threading.Thread as a member variable. Then the implementation of this interfaces System.Threading.Thread.Interrupt method can call System.Threading.Thread.Interrupt on that Thread. Before attempting this, make sure that you fully understand what System.Threading.Thread.Interrupt does and doesn't do. Also make sure that you clear the Job's member reference to the Thread when the Execute(..) method exits (preferably in a finally block).
该接口定义了 Interrupt 方法,当调用 Scheduler.Shutdown() 方法时,Quartz.IScheduler 将会调用该方法来中断正在运行的任务。这就意味着,我们需要自己实现中断方法来停止当前的 Job 。 通常我们可以通过在任务执行时拿到当前工作的线程,并在中断时调用线程 Abort 方法的方式来终止当前任务。
public class CommonInterruptableJob : IInterruptableJob { private Thread _currentThread; public void Execute(IJobExecutionContext context) { _currentThread = Thread.CurrentThread; try { //TODO:编写你的任务代码 } finally { _currentThread = null; } } public void Interrupt() { if (_currentThread != null) { _currentThread.Abort(); _currentThread = null; } } }
这种方法简单粗暴,在一些要求不太严格的情况下表现令人满意。更为优雅的方式是定义布尔型字段 _stop 默认为 false ,在 Interrupt 方法被调用时将其设置为 true 。在 Execute 时不断检测该字段的值,并在合适的时机退出处理。
public class NiceInterruptableJob : IInterruptableJob { private bool _stop; public void Execute(IJobExecutionContext context) { //假设我的任务是循环 1000 次处理某数据 for (var i = 0; !_stop && i < 1000; i++) { //TODO:处理代码 } } public void Interrupt() { _stop = true; } }
本片文章对 Quartz.NET 进行了一个简单的介绍,且展示了两种不同的实现任务终止的方式。方式一简单粗暴,编写简单,适合大多数要求不太严格的场合。方式二更加优雅,对退出时机的掌控更加精确,不容易出现危险,但编写更加复杂。