Running a WF Program Instance


Now that we know how to create a new instance of a WF program, it's time to run it.

As we have already seen in some examples, calling CreateWorkflow is not enough; the Start method of WorkflowInstance must be called in order to begin the execution of the instance (and resume the initial, implicit, bookmark whose resumption point is the Execute method of the root activity).

It is here that the threading model employed by the WF runtime comes to the forefront. In Chapter 3, we learned that the scheduler (for a particular WF program instance) will only use a single CLR thread in its dispatcher loop. This ensures that within a WF program instance, the code for at most one activity can actually be executing at a given point in time. What is relevant to the application that hosts the WF runtime is where that CLR thread comes from.

The WF runtime never creates threads on its own, but instead relies upon a runtime service to obtain threads on which it can run WF program instances.

The relevant runtime service is called WorkflowSchedulerService, which provides an abstraction for a specific host-controllable threading policy (that drives the WF scheduler) and is shown in Listing 5.8.

Listing 5.8. WorkflowSchedulerService

 namespace System.Workflow.Runtime.Hosting {   public abstract class WorkflowSchedulerService     : WorkflowRuntimeService   {     protected internal abstract void Schedule(       WaitCallback callback,       Guid workflowInstanceId     );     /* *** other members *** */   } } 


The default implementation of this service is the DefaultWorkflowSchedulerService, which is shown in Listing 5.9.

Listing 5.9. DefaultWorkflowSchedulerService

 namespace System.Workflow.Runtime.Hosting {   public class DefaultWorkflowSchedulerService     : WorkflowSchedulerService   {     public DefaultWorkflowSchedulerService();     public DefaultWorkflowSchedulerService(       int maxSimultaneousWorkflows);     public int MaxSimultaneousWorkflows { get; }     /* *** other members *** */   } } 


It is crucial to understand that the WF runtime does not create any CLR threads of its own. The threads that execute any code within the WF runtime (and WF program instances) are always threads that are owned and managed by the application that hosts the WF runtime. The host calls the methods of WorkflowInstance objects on any threads it wants. Not only can different program instances be created on different threads, but two or more WorkflowInstance objects representing the same WF program instance can be obtained on different threads.

Various operations of WorkflowInstance can be invoked on different threadsthe threads within a WorkflowInstance object are simply visitors, as depicted in Figure 5.7.

Figure 5.7. Threads visiting a WF program instance


Although operations on WorkflowInstance can be invoked on arbitrary threads, the WF scheduler hosted within the program instance is serviced by a single thread. The WF runtime guarantees that no other thread can interfere or service the scheduler while its dispatcher loop is actively processing work items. To be clear, the hosting application can invoke methods of WorkflowInstance on separate threads concurrentlythis does not affect the scheduler executing the activities on a dedicated thread (for an episode of execution).

Finally, although the scheduler of each WF program instance is serviced by a distinct CLR thread, a WF program instance can always run concurrently with other instances, without any conflict.

Because WF programs typically run (in a logical sense) for a long time, with their lifecycle potentially distributed across multiple application domains or machines, it is natural that the Start method of WorkflowInstance does not block awaiting the completion of the instance.

Instead, the Start method internally calls AEC.ExecuteActivity, passing the root activity as a parameter. Thus, the execution of the root activity is scheduled in exactly the same way as composite activities schedule the execution of their child activities. The Start method returns to the caller (the application hosting the WF runtime) after the work item for the root activity's execution has been scheduled.

At this point, the WF runtime requires a thread in order to dispatch the work item in the scheduler work queue. To obtain the thread, the WorkflowSchedulerService is utilized. Specifically, the Schedule method of WorkflowSchedulerService is called, with the WaitCallback representing the scheduler dispatcher loop passed as a parameter. A typical implementation of the Schedule method invokes the callback on a distinct CLR thread using the CLR thread pool. This is essentially what the DefaultWorkflowSchedulerService does, so after the Start method returns, you will see the WF program instance running on a different thread obtained from the CLR thread pool. This asynchronous pattern is illustrated (simplistically) with the following snippet:

 protected override void Schedule(WaitCallback callback,   Guid workflowInstanceId) {   System.Threading.ThreadPool.QueueUserWorkItem(     callback, workflowInstanceId); } 


Threads in Applications

In some scenarios, the application hosting the WF runtime has specific threading requirements dictated by the underlying programming model of the application. For instance, it may be desirable to have the host application explicitly provide the thread to be used by the scheduler in its dispatch loop, rather than having that thread come from the CLR thread pool. This is especially true for several popular UI programming models. Applications written using Windows Forms, Windows Presentation Foundation (WPF), and ASP.NET must follow the threading model of the chosen technology.

For example, Windows Forms requires that a UI control (say, a listbox) can only be accessed by the thread that created the control. This means that a background thread (perhaps in charge of obtaining data) cannot write values directly into the listbox. There is a requirement to explicitly switch what is known as the synchronization context, when operations are performed on background threads. When a Windows Forms application hosts the WF runtime and uses the DefaultWorkflowSchedulerService to run WF programs, we run into a problem if an activity calls a service that attempts to update a UI control. In this situation, we will see an InvalidOperationException stating

 The calling thread may not access this object because the object is owned by a different thread. 


One simple way to get around this issue is to use a service like the SynchContextSwitcher that is shown in Listing 5.10.

Listing 5.10. SynchContextSwitcher

 using System; using System.Threading; namespace EssentialWF.Services {   public sealed class SynchContextSwitcher   {     private SynchronizationContext originalContext = null;     public SynchContextSwitcher()     {       // cache the SynchronizationContext.Current       // on the host thread       this.originalContext = SynchronizationContext.Current;     }     public SynchronizationContext SynchContext     {       get { return this.originalContext; }     }   } } 


The SynchContextSwitcher caches the SynchronizationContext associated with the thread on which its constructor is executed.

Assuming that the Windows Forms application creates an instance of this service on the UI thread, the SynchronizationContext of the UI thread will be cached. The WF runtime can continue to use the DefaultWorkflowSchedulerService and execute WF program instances on CLR thread pool threads:

 // The following code is executed on the UI thread using (WorkflowRuntime runtime = new WorkflowRuntime()) {   runtime.AddService(new SynchContextSwitcher());   WorkflowInstance instance = runtime.CreateWorkflow(...);   instance.Start(); } 


A service upon which activities rely (let's call it MyService), and which updates the UI of the Windows Formsbased application in which it is running, can use the SynchContextSwitcher as shown here:

 class MyService {   private WorkflowRuntime runtime;   public void DoSomething(object o)   {     SynchContextSwitcher switcher =       runtime.GetService<SynchContextSwitcher>();     switcher.SynchContext.Post(ActualDoSomething, o);   }   public void ActualDoSomething(object o)   {     // update the UI here   } } 


A better approach might be to implement a custom WorkflowSchedulerService that is intrinsically aware of the CLR SynchronizationContext and simply schedules the execution of activities under the synchronization context of the UI thread. Put differently, a custom WorkflowSchedulerService implementation can encapsulate synchronization context and eliminate the need to manually perform switching, which allows activity execution to proceed on the UI thread. This design frees up individual services from the burden of explicitly switching the synchronization context.

A custom WorkflowSchedulerService is shown is Listing 5.11.

Listing 5.11. SynchronizationContextSchedulerService

 using System; using System.Threading; using System.Workflow.Runtime.Hosting; namespace EssentialWF.Services {   public sealed class SynchronizationContextSchedulerService :     WorkflowSchedulerService   {     bool synchronousDispatch = true;     SynchronizationContext originalContext = null;     public SynchronizationContextSchedulerService() : this(true){}     public SynchronizationContextSchedulerService(       bool synchronousDispatch)     {       this.originalContext = SynchronizationContext.Current;       this.synchronousDispatch = synchronousDispatch;     }     public bool SynchronousDispatch     {       get { return this.synchronousDispatch; }     }     protected override void Schedule(WaitCallback callback, Guid       workflowInstanceId)     {       // If the saved context from the thread that instantiated this       // service is null, try obtaining the SynchronizationContext       // of the current thread       SynchronizationContext ctx = this.originalContext != null ?         this.originalContext : SynchronizationContext.Current;       if (ctx != null)       {         if (this.SynchronousDispatch)           ctx.Send(delegate {callback(workflowInstanceId);}, null);         else           ctx.Post(delegate {callback(workflowInstanceId);}, null);       }       else // run the scheduler without SynchronizationContext         callback(workflowInstanceId);     }     ...   } } 


The approach outlined here works not just for Windows Forms but also for WPF and ASP.NET, because each of these technologies provides an implementation of SynchronizationContext for their respective threading models. The technique works especially well in situations when the WF program instance runs in short bursts, in response to user input, and then waits for the next stimulus.

Because SynchronizationContextSchedulerService.Schedule uses the value of Synchronization.Current, it allows the host application to dictate the synchronization context explicitly by calling SynchronizationContext.SetSynchronizationContext before a call to WorkflowInstance.Start is made.

The application hosting the WF runtime must add the custom service to the WorkflowRuntime, like so:

 using (WorkflowRuntime runtime = new WorkflowRuntime()) {   runtime.AddService(new SynchronizationContextSchedulerService());   ...   WorkflowInstance instance = runtime.CreateWorkflow(...);   // Execute on the current thread and its synchronization context   instance.Start();   ... } 


The implementation of the SynchronizationContextSchedulerService allows the host application to execute a WF program instance on a thread of its choosing. This is similar to the System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService that ships with WF, except that our implementation has an advantage in that it does not require the host application to explicitly call a special method (ManualWorkflowSchedulerService.RunWorkflow) to run the instance. Moreover, our implementation is aware of the CLR SynchronizationContext, whereas ManualWorkflowSchedulerService is not.




Essential Windows Workflow Foundation
Essential Windows Workflow Foundation
ISBN: 0321399838
EAN: 2147483647
Year: 2006
Pages: 97

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net