C#中的简单调度器






4.84/5 (38投票s)
如何为桌面和Web应用程序构建简单的C#调度器。
引言
许多桌面和Web应用程序都需要一个调度器(Scheduler)。调度器指的是一个控制任务(Jobs)并行执行的框架。这些任务可以只执行一次,也可以重复执行。因此,我们希望有一个框架,只需要实现任务本身并将其挂接到框架上,就可以执行一次或多次任务。
本文的目的是展示如何开发这个调度器,并详细解释其实现的技巧和思路。您可能会注意到,使用合适的设计模式和简单的解决方案,其结果可能出奇地简单明了。
这里描述的调度器是在Microsoft .NET类库项目中开发的。Microsoft .NET控制台项目使用该DLL类库来实现和执行几个任务。本文接下来的部分将详细展示如何做到这一点。
背景
在开发企业级应用程序时,调度器一直是架构师们关注的问题。之所以需要这种框架,是因为自动化并行任务极大地减轻了用户的工作量,因为它将重复性的活动从用户手中解放出来。
市面上有许多现成的解决方案。例如,Java提供了几个调度器,如Quartz。而Microsoft .NET开发者可以利用Windows服务。
然而,有些情况下您可能无法使用这些工具。例如,您可能需要在共享服务器上部署一个ASP.NET应用程序,而Windows服务的使用是不允许的。
即使可以使用框架来处理调度事件,但了解如何实现这样一个解决方案仍然非常有趣。在这种情况下,它结合了恰当使用线程和实现模板方法设计模式。
架构
本节讨论架构。首先,显示并解释类图。
类图由框架(在DLL类库中)和分派任务的执行程序组成。`Job`和`JobManager`类构成了框架;实现这个框架就是这么简单。`RepeatableJob`、`RepeatableJob2`和`SingleExecutionJob`是任务的实现。它们的名称揭示了它们的主要特性。
需要实现`Job`类以执行可重复或单次执行的任务。`JobManager`类负责收集和执行所有可用的任务(继承自`Job`的类)。
`RepeatableJob`和`RepeatableJob2`是永不结束的任务,意味着它们的代码在设定的时间间隔内始终被执行。`SingleExecutionJob`的代码只运行一次。这些任务的唯一目的是打印一条消息,说明它们已被执行。当程序运行时,这很好地说明了它们在控制台中的行为。
剩余的类`Program`只是实例化`JobManager`并运行它。`JobManager`异步执行任务。
在理解了应用程序提供的类的总体思想之后,现在就可以理解这个调度器架构的核心主要思路了。`JobManager`收集`Job`类的所有任务实现。这些任务实现必须位于使用包含调度器框架的DLL的项目中。收集完这些任务后,每个任务都在新线程中启动。`Job`类有一个`ExecuteTask()`方法,它触发任务的实现。如果任务只需执行一次,任务完成后,该任务即结束执行,线程终止。如果任务是可重复的,任务将在任务实现提供的间隔内重复执行。请注意,`Job`类包含一些已实现的方法和一些必须由其实现提供的方1法。这种技术属于模板方法设计模式。
下一节将更详细地解释代码中最相关的部分,以便您能实际了解如何实现调度器。
使用代码
`Job`类显然是开始讨论的类。通过查看它,可以更好地理解框架。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
namespace SchedulerManager.Mechanism
{
/// <summary>
/// Classes which extend this abstract class are Jobs which will be
/// started as soon as the application starts. These Jobs are executed
/// asynchronously from the Web Application.
/// </summary>
public abstract class Job
{
/// <summary>
/// Execute the Job itself, one ore repeatedly, depending on
/// the job implementation.
/// </summary>
public void ExecuteJob()
{
if (IsRepeatable())
{
// execute the job in intervals determined by the methd
// GetRepetionIntervalTime()
while (true)
{
DoJob();
Thread.Sleep(GetRepetitionIntervalTime());
}
}
// since there is no repetetion, simply execute the job
else
{
DoJob();
}
}
/// <summary>
/// If this method is overriden, on can get within the job
/// parameters set just before the job is started. In this
/// situation the application is running and the use may have
/// access to resources which he/she has not during the thread
/// execution. For instance, in a web application, the user has
/// no access to the application context, when the thread is running.
/// Note that this method must not be overriden. It is optional.
/// </summary>
/// <returns>Parameters to be used in the job.</returns>
public virtual Object GetParameters()
{
return null;
}
/// <summary>
/// Get the Job´s Name. This name uniquely identifies the Job.
/// </summary>
/// <returns>Job´s name.</returns>
public abstract String GetName();
/// <summary>
/// The job to be executed.
/// </summary>
public abstract void DoJob();
/// <summary>
/// Determines whether a Job is to be repeated after a
/// certain amount of time.
/// </summary>
/// <returns>True in case the Job is to be repeated, false otherwise.</returns>
public abstract bool IsRepeatable();
/// <summary>
/// The amount of time, in milliseconds, which the Job has to wait until it is started
/// over. This method is only useful if IJob.IsRepeatable() is true, otherwise
/// its implementation is ignored.
/// </summary>
/// <returns>Interval time between this job executions.</returns>
public abstract int GetRepetitionIntervalTime();
}
}
这个类注释得很完善,阅读起来很方便。这里的想法是提供任务执行所需的一切。方法的解释如下:
DoJob()
- 在这里提供任务的执行。这意味着所有需要完成的工作都必须放在这个方法里面。IsRepeatable()
- 确定任务是否需要重复执行。GetRepetitionIntervalTime()
- 返回任务在下次执行之前需要等待的间隔(以毫秒为单位),如果任务是可重复的,则当然如此。GetName()
- 唯一标识任务。这个名称在持有它们的程序集中的任务实现之间必须是唯一的,否则会发生意外行为,这一点非常重要。GetParameters()
- 当希望将参数传递给任务执行时,需要实现此方法。要访问输入的参数,只需在任务实现(`DoJob()`方法)中调用此方法即可。ExecuteJob()
- 此方法执行任务本身。它调用`DoJob()`方法。请注意,如果方法需要重复运行,`DoJob()`将在循环中执行,否则只调用一次。这里应用了模板方法设计模式。`ExecuteJob()`方法的执行基于其类的方法实现。
下一个需要理解的类是`JobManager`,如下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
using log4net;
using SchedulerManager.Log4Net;
namespace SchedulerManager.Mechanism
{
/// <summary>
/// Job mechanism manager.
/// </summary>
public class JobManager
{
private ILog log = LogManager.GetLogger(Log4NetConstants.SCHEDULER_LOGGER);
/// <summary>
/// Execute all Jobs.
/// </summary>
public void ExecuteAllJobs()
{
log.Debug("Begin Method");
try
{
// get all job implementations of this assembly.
IEnumerable<Type> jobs = GetAllTypesImplementingInterface(typeof(Job));
// execute each job
if (jobs != null && jobs.Count() > 0)
{
Job instanceJob = null;
Thread thread = null;
foreach (Type job in jobs)
{
// only instantiate the job its implementation is "real"
if (IsRealClass(job))
{
try
{
// instantiate job by reflection
instanceJob = (Job)Activator.CreateInstance(job);
log.Debug(String.Format(
"The Job \"{0}\" has been instantiated successfully.",
instanceJob.GetName()));
// create thread for this job execution method
thread = new Thread(new ThreadStart(instanceJob.ExecuteJob));
// start thread executing the job
thread.Start();
log.Debug(String.Format(
"The Job \"{0}\" has its thread started successfully.",
instanceJob.GetName()));
}
catch (Exception ex)
{
log.Error(String.Format("The Job \"{0}\" could not " +
"be instantiated or executed.", job.Name), ex);
}
}
else
{
log.Error(String.Format(
"The Job \"{0}\" cannot be instantiated.", job.FullName));
}
}
}
}
catch (Exception ex)
{
log.Error("An error has occured while instantiating " +
"or executing Jobs for the Scheduler Framework.", ex);
}
log.Debug("End Method");
}
/// <summary>
/// Returns all types in the current AppDomain implementing the interface or inheriting the type.
/// </summary>
private IEnumerable<Type> GetAllTypesImplementingInterface(Type desiredType)
{
return AppDomain
.CurrentDomain
.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => desiredType.IsAssignableFrom(type));
}
/// <summary>
/// Determine whether the object is real - non-abstract, non-generic-needed, non-interface class.
/// </summary>
/// <param name="testType">Type to be verified.</param>
/// <returns>True in case the class is real, false otherwise.</returns>
public static bool IsRealClass(Type testType)
{
return testType.IsAbstract == false
&& testType.IsGenericTypeDefinition == false
&& testType.IsInterface == false;
}
}
}
同样,这个类注释得很完善,以便于理解。看看`ExecuteAllJobs()`方法。这个方法从正在执行的任务的程序集中收集所有任务实现,并在单独的线程中运行它们。请注意,这个解决方案非常简单。它没有死锁或复杂线程交互的危险。此外,由于每个线程独立运行,因此在此框架中调试和查找错误非常容易。
最后一点需要注意的是任务的实现。下面显示了`SimgleExecutionJob`和`RepeatableJob`的实现。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SchedulerManager.Mechanism;
namespace SchedulerConsoleApp.Jobs
{
/// <summary>
/// A simple job which is executed only once.
/// </summary>
class SimgleExecutionJob : Job
{
/// <summary>
/// Get the Job Name, which reflects the class name.
/// </summary>
/// <returns>The class Name.</returns>
public override string GetName()
{
return this.GetType().Name;
}
/// <summary>
/// Execute the Job itself. Just print a message.
/// </summary>
public override void DoJob()
{
System.Console.WriteLine(String.Format("The Job \"{0}\" was executed.",
this.GetName()));
}
/// <summary>
/// Determines this job is not repeatable.
/// </summary>
/// <returns>Returns false because this job is not repeatable.</returns>
public override bool IsRepeatable()
{
return false;
}
/// <summary>
/// In case this method is executed NotImplementedException is thrown
/// because this method is not to to be used. This method is never used
/// because it serves the purpose of stating the interval of which the job
/// will be executed repeatedly. Since this job is a single-execution one,
/// this method is rendered useless.
/// </summary>
/// <returns>Returns nothing because this method is not to be used.</returns>
public override int GetRepetitionIntervalTime()
{
throw new NotImplementedException();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SchedulerManager.Mechanism;
namespace SchedulerConsoleApp.Jobs
{
/// <summary>
/// A simple repeatable Job.
/// </summary>
class RepeatableJob : Job
{
/// <summary>
/// Counter used to count the number of times this job has been
/// executed.
/// </summary>
private int counter = 0;
/// <summary>
/// Get the Job Name, which reflects the class name.
/// </summary>
/// <returns>The class Name.</returns>
public override string GetName()
{
return this.GetType().Name;
}
/// <summary>
/// Execute the Job itself. Just print a message.
/// </summary>
public override void DoJob()
{
System.Console.WriteLine(String.Format(
"This is the execution number \"{0}\" of the Job \"{1}\".",
counter.ToString(), this.GetName()));
counter++;
}
/// <summary>
/// Determines this job is repeatable.
/// </summary>
/// <returns>Returns true because this job is repeatable.</returns>
public override bool IsRepeatable()
{
return true;
}
/// <summary>
/// Determines that this job is to be executed again after
/// 1 sec.
/// </summary>
/// <returns>1 sec, which is the interval this job is to be
/// executed repeatadly.</returns>
public override int GetRepetitionIntervalTime()
{
return 1000;
}
}
}
请注意它们的简单性:`SimgleExecutionJob`根据其类名提供标识符,实现打印消息的任务,并表明该任务不可重复。`RepeatableJob`确实表明任务是可重复的,提供了执行间隔,给出了用作其标识符的类名,并将其任务定义为简单的消息打印。
编译和运行代码
当打开代码并尝试编译时,会出现错误,指出缺少log4net库。这是因为在CodeProject文章中不允许上传.dll文件,因此log4net.dll已被移除。因此,要正确设置解决方案并成功构建,请点击此处下载log4net库。 然后,在SchedulerManager-noexe文件夹中创建一个名为libraries的子目录。在libraries目录中复制您刚刚下载的log4net.dll文件。最后,刷新SchedulerManager项目引用,您应该看到log4net引用没有警告标志。编译解决方案并运行SchedulerConsoleApp项目。
讨论
首先,正如之前所述,请注意此解决方案的简单性。简单地说,就是收集所有任务实现并在单独的线程中执行它们。这种直接性使得向此框架添加更多功能变得容易。
关于新功能,您可以考虑将任务的需求存储在数据库中。例如,任务是否可重复、执行间隔等都可以存储在数据库中,每行由其唯一名称标识。这使得维护更加容易,因为更改其参数执行不再需要代码更改,只需更改数据库即可。
其他功能,如处理任务管理的接口、运行、暂停和取消任务执行的能力也可以实现。但请注意,尽管这些功能看起来不错且花哨,但在大多数应用程序中很少需要。
许多人认为在Web应用程序中处理线程非常危险,因为它可能危及应用程序服务器。根据作者的经验,至少在处理Internet Information Services时,事实并非如此。