《WF编程》系列之42 – 承载工作流:调度(Scheduling)服务

在WF中,调度服务的职责是将工作流安排到线程上来执行。WF提供了两个调度服务:DefaultWorkflowSchedulerService和ManualWorkflowSchedulerService。如果我们不去显式的配置调度服务,Runtime会使用默认的调度器(DefaultWorkflowSchedulerService)。这两个调度器都继承自 WorkflowSchedulerService类。如果我们需要自定义的调度逻辑,也可以从此基类继承,并重写它的虚方法。

6_2_SchedulerServiceClass

工作流Runtime通过调用Schedule和Cancel方法来规划工作流的执行过程。默认调度服务会将工作流安排到进程内的CLR线程池中运行。所以在默认情况下,工作流是在后台线程中异步的执行,在我们的几乎所有示例中,都通过AutoResetEvent锁住主线程来等待工作流完成,也是由于这个原因。而使用手工调度服务的宿主必须为工作流Runtime贡献自己的线程,Runtime将使用这个线程来执行工作流。通过使用手工调度服务,可以同步的执行工作流。

6.2.1 调度服务和线程

让我们创建一个只包含一个code活动的工作流,代码如下:

public sealed partial class Workflow1: SequentialWorkflowActivity
{
public Workflow1()
{
InitializeComponent();
}
private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
Console.WriteLine("Hello from {0}", this.QualifiedName);
Console.WriteLine(" I am running on thread {0}",
Thread.CurrentThread.ManagedThreadId);
}
}

为了区别两种调度服务,我们将分别用手工调度器和默认调度器运行工作流。在本例中,我们通过代码的方式将调度服务添加到工作流Runtime中,具体代码如下:

WorkflowRuntime runtime1 = new WorkflowRuntime();
ManualWorkflowSchedulerService scheduler;
scheduler = new ManualWorkflowSchedulerService();
runtime1.AddService(scheduler);
WorkflowInstance instance;
instance = runtime1.CreateWorkflow(typeof(Workflow1));
Console.WriteLine("Setting up workflow from thread {0}",
Thread.CurrentThread.ManagedThreadId);
instance.Start();
scheduler.RunWorkflow(instance.InstanceId);
WorkflowRuntime runtime2 = new WorkflowRuntime();
instance = runtime2.CreateWorkflow(typeof(Workflow1));
instance.Start();

本例中我们有两个工作流Runtime。我们将runtime1的调度服务配置为手工调度器,而runtime2的调度服务则不做显式配置,所以 runtime2将使用默认调度器。我们的代码在执行这两个工作流之前输出了当前的线程ID,然后在工作流的执行过程中再输出工作流的线程ID。

请注意,通过手工调度器运行工作流分为两个步骤。首先,我们调用工作流实例的Start方法来安排工作流运行。但调用Start方法只是让 Runtime做好准备,而不是真正运行工作流。想要执行工作流,我们还得显式调用手工调度服务的RunWorkflow方法并传入工作流实例ID。手工调度服务将使用调用方的线程来同步的执行工作流。这个过程就是宿主贡献线程的过程。

runtime2使用默认调度服务,所以我们只需要调用工作流实例的Start方法即可。默认调度器会自动将工作流安排到线程池中。从下图中,我们可以看到程序使用了两个不同的线程来运行工作流。当使用runtime1时,工作流将使用调用方的线程,而使用runtime2时,工作流将使用不同的线程。

6_3_ScheduleService

6.2.2 调度服务和配置

使用应用程序配置文件来配置runtime的一个好处是我们可以很轻松的改变服务和服务的参数,而不需要重新编译应用程序。让我们来看看上个示例的程序如何用配置文件来配置:

请注意我们使用了两个工作流配置节,并且都拥有节处理器。ManualRuntime节配置了手工调度器,DefaultRuntime节配置了默认调度器。每个服务都有附加的配置参数,接下来我们会讨论这些参数,但是现在我们必须修改一下代码来使用配置文件。

WorkflowRuntime runtime1 = new WorkflowRuntime("ManualRuntime");
ManualWorkflowSchedulerService scheduler = runtime1.GetService();
WorkflowInstance instance;
instance = runtime1.CreateWorkflow(typeof(Workflow1));
Console.WriteLine("Setting up workflow from thread {0}",
Thread.CurrentThread.ManagedThreadId);
instance.Start();
scheduler.RunWorkflow(instance.InstanceId);
WorkflowRuntime runtime2 = new WorkflowRuntime("DefaultRuntime");
instance = runtime2.CreateWorkflow(typeof(Workflow1));
instance.Start();

这段代码通过传入配置节名称来初始化每个Runtime。和之前的代码相比,它们的的主要区别是在代码中获得手工调度服务的方式。因为我们没有显式创建手工调度服务的实例,所以需要从Runtime获取这个引用。WorkflowRuntime类的GetService方法会根据类型参数找到相应的服务并返回。

6.2.2.1 调度参数

每个调度服务都可以利用参数来调节自身的行为。手工调度器有一个useActiveTimers参数,我们可以在配置文件中设置它,也可以通过向该服务的构造函数传递参数来设置它。当userActiveTimers为false(默认值)时,宿主会在DelayActivity到期后恢复工作流。当这个参数的值为true时,服务将会建立一个后台线程并使用内存计数器来自动恢复工作流。

默认调度器有一个maxSimultaneousWorkflows参数。这个参数控制了在线程池中同时运行的工作流实例数目。在单处理器计算机中,maxSimultaneousWorkflows的默认值是5,而在多处理器计算机中,他的默认值是5 * Environment.ProcessorCount * 0.8。

进程范围内的CLR线程池并不是可以无限量的创建线程的。默认的最大数量是25 * 计算机的处理器数量。如果应用程序运行了大量工作流,调节maxSimultaneousWorkflow参数是很有必要的,因为混乱的线程池可能导致死锁和应用程序挂起,而通过调节maxSimultaneousWorkflow参数可以在工作流吞吐量和线程池中的自由线程之间取得适当的平衡。

6.2.3 选择正确的调度服务

现在我们有两种调度服务可以选择,那么一个很明显的问题摆在眼前 – 用哪个调度服务比较好?大多数智能客户端应用程序使用默认调度服务。使用Windows Forms和WPF技术的应用程序可能希望在执行工作流的时候用户界面仍然可以响应,所以它需要在线程池中的线程上异步执行工作流。

服务器端应用程序的工作方式就不一样了。服务器端应用程序总是希望使用最少的线程去尽可能多的处理客户端请求。Web应用程序和Web Services的ASP.NET Runtime已经使用线程池中的线程来处理HTTP请求了。异步的工作流执行过程使用默认调度器,这只会为每个请求都捆绑一个附加线程。所以服务器端应用程序通常会使用手工调度器并将处理请求的线程贡献给工作流。

One Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注