Windows Workflow Foundation (WF4) Activities and Threads

Most of the time in software when we say “Parallel” we mean that multiple threads are being used to do work concurrently.  Because of this, many customers ask if the Parallel activity works this way in Windows Workflow Foundation.  I know there is a great deal of confusion about activities and threads and in the next few posts I hope to bring some clarity.

This post is a part of a series on WF, Parallelism and Threading

  1. Windows Workflow Foundation (WF4) Activities and Threads
  2. The Workflow Parallel Activity and Task Parallelism
  3. Windows Workflow (WF4) Task Parallelism with Sequences
  4. Windows Workflow Foundation (WF4) ParallelFor Activity
  5. Windows Workflow Foundation (WF4) - Parallel and ParallelFor Behavior (sample)

Activity Lifecycle

Phase 1: Scheduling

Activities are scheduled by a parent activity (or the Workflow Runtime if they are the root activity).  The Workflow runtime maintains a queue/stack of child activities that are scheduled (the class is actually called a Quack<T>).  The Workflow Runtime has an ActivityExecutor that invokes each activity when it is popped off the Quack.  At that point, the activity moves into the Executing state.

Phase 2: Executing

Once an activity starts Executing, it will remain in the Executing state until it Completes, Faults or is Canceled.

image

How do we know when an activity is complete?  An activity is complete when it completes it’s Execute method and there are no outstanding bookmarks for the activity or any of it’s children.

Faulted

If an unhandled exception occurs in the activity then it moves into the Faulted state.

Canceled

The parent activity or the Workflow Runtime may cancel an activity then it moves into the Canceled state.

Workflow Lifecycle

The Workflow begins by scheduling the root activity.  From there it is up to the root activity to schedule any other activities that it uses.  Any activity can serve as the root activity making it possible to have a 1 activity workflow or very large workflows composed of activities within activities.  When the root activity is added to the Quack, the Activity Executor takes over.

The Activity Executor executes only one activity at a time as the activity rises to the top of the Quack.  If the Quack is empty and there are no outstanding bookmarks then the Workflow is completed.  If the Quack is empty and there are outstanding bookmarks then the Workflow is idle.

Activities and Threads

You will notice that there are no activities like “Create Thread” or “Wait” or “Join”.  Sometimes people think they want such thread primitives. but in reality having these would likely encourage the wrong behavior.  Rather than spinning up lots of threads, it’s much better to queue work that needs to be done and let the thread pool do it (see AppFabric.tv - Threading with Jeff Richter).

As an Activity author you can’t make any assumptions about what thread your activity will be invoked on or how that thread will relate to any other thread used by any other activity.  In fact, you must assume that (in the worst case) that every activity could be invoked on a different thread.

Windows Workflow 3.5 allowed you to control the threading behavior with a WorkflowSchedulerService.  Windows Workflow 4 achieves the same goal by using a SynchronizationContext.  The SynchronizationContext used determines the threading behavior you will see as you run your workflow.

WorklflowInvoker uses a class called PumpBasedSynchronizationContext which results in all activities in the Workflow being invoked on the same thread.

WorkflowApplication uses the default SynchronizationContext which simply calls QueueUserWorkItem to queue work. 

The following output shows the difference with an activity that goes idle. 

WorkflowInvoker [Thread] WorkflowApplication [Thread]

[63] Test Thread [63] Start 1 [63] 1 Idle [63] Start 2 [63] 2 Idle [63] Start 3 [63] 3 Idle [63] End 1 [63] End 2 [63] End 3

[186] Test Thread [169] Start 1 [169] 1 Idle [169] Start 2 [169] 2 Idle [169] Start 3 [169] 3 Idle [169] End 1 [169] End 2 [169] End 3

As you can see WorkflowInvoker invoked all activities on the same thread as the Test Thread that started it.  While WorkflowApplication invoked the activities on a different thread. In this case all activities were invoked on the same thread, but I have seen cases where the activities were invoked on 2 or 3 different threads.

One At A Time But Async is Supported

The ActivityExecutor invokes one and only one activity at a time.  This does not mean that you cannot do asynchronous operations.  In fact, this is one big advantage of WF4 over WF3.5 (where you could not safely do async).  You can do Asynchronous operations by using the AsyncCodeActivity (see Creating Asynchronous Activities in WF) or by using a NativeActivity with a NoPersistHandle.

If you are creating an activity that is doing any kind of I/O you should create an async activity to do the work.  When the work is complete, the WorkflowRuntime will schedule the callback resulting in an insanely efficient workflow.

Here is an example of an AsyncCodeActivity implemented using a Task

 /// <summary>
 /// An async class that sleeps for a duration
 /// </summary>
 /// <remarks>
 /// While similar to the Delay activity,
 /// this class does not create a bookmark
 /// allowing you to simulate an async operation 
 /// when testing
 /// </remarks>
 public class TestAsync : AsyncCodeActivity
 {
     /// <summary>
     /// The number of milliseconds to sleep
     /// </summary>
     public InArgument<int> Sleep { get; set; }
  
     protected override IAsyncResult BeginExecute(
         AsyncCodeActivityContext context,
         AsyncCallback callback,
         object state)
     {
         var sleep = this.Sleep.Get(context);
  
         // Using a Task to start an async operation
         var task = Task.Factory.StartNew(_ => Thread.Sleep(sleep), state);
  
         if (callback != null)
         {
             task.ContinueWith(res => callback(task));
         }
  
         return task;
     }
  
     protected override void EndExecute(
         AsyncCodeActivityContext context,
         IAsyncResult result)
     {
         // Access the result or just Wait
         // This is important in case there was an exception
         // on the background thread.
         ((Task)result).Wait();
     }
 }

Summary

Here is what you need to know about threads and activities

  • As an activity author, don’t assume anything about threads
  • When invoking a workflow, if you want to force a synchronous call on the same thread use WorkflowInvoker (but remember, no bookmarks are allowed in this case)
  • The ActivityExecutor invokes one and only one activity at a time so use and create async activities whenever possible
  • When you want to use workflow in an environment with a different synchronization context use WorkflowApplication.SynchronizationContext to set the synchronization context you want to use.
  • If you want fine-grained control over the threading behavior you can implement your own SynchronizationContext.

I’ve got much more to come as we discuss how the Parallel activity works in my next post.

Happy Coding!

Ron Jacobs

https://blogs.msdn.com/rjacobs

Twitter: @ronljacobs https://twitter.com/ronljacobs