65.9K
CodeProject 正在变化。 阅读更多。
Home

C#中的简单调度器

2013年5月12日

CPOL

7分钟阅读

viewsIcon

441412

downloadIcon

15653

如何为桌面和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时,事实并非如此。

© . All rights reserved.