Dispatch? I'd like to make a call. The difference between Synchronization Context, Task Scheduler and Dispatcher


I recently had to deal with cleaning up Dispatcher  based UI update code in a WPF application. I am struck after so many years that like garbage collection (think IDisposable) and threading, there still does not exist a clear understanding or explanation of how to marshal across threads, the benefits of using constructs like the SynchronizationContext to do so, and what problem(s) it solves.

I thought it might be instructive to demonstrate the various techniques available to developers using the WPF framework for illustrative purposes. Keep in mind that the concepts outlined here apply not just in WPF, but anywhere where business logic code meets UI presentation.

Given a Dispatcher object and the SynchronizationContext object, which one should you choose, and what are the compelling reasons for doing so?

Note: This post was inspired from an answer I posted on StackOverflow to this very question.

Perhaps it would help to explain what problem the SynchronizationContext object is trying to solve in the first place:


  1. It provides a way to queue a unit work to a context. Notice that this isn't thread specific, and so we avoid the problem of thread affinity.
  2. Every thread gas a "current" context, but that context might be shared across threads i.e. a context is not necessarily unique.
  3. Contexts keep a count of outstanding asynchronous operations. This count is often, but not always incremented/decremented on capture/queuing. 


It would be fair to say, judging from the criteria above, that using the SynchronizationContext
 would be preferable to the Dispatcher.

But there are even more compelling reasons to do so:

Separation of concerns


By using the SynchronizationContext to handle executing code on the UI thread, you can now easily separate your operations from the display via decoupled interface(s). Which leads to the next point:

Easier unit testing


If you have ever tried to mock an object as complex as the Dispatcher versus the SynchronizationContext, which has far fewer methods to deal with, you will quickly come to appreciate the far simpler interface offered by the SynchronizationContext.

IoC (Inversion of Control) and Dependency Injection


As you have already seen, the SynchronizationContext is implemented across many UI frameworks: WinForms, WPF, APS.NET, etc. If you write your code to interface to one set of APIs, your code becomes more portable and simpler to maintain as well as test.

By way of example:

Note: In have left out exception handling to make the code clear.


Suppose we have a WPF application that has a single button. Upon clicking that button, you will start a long process of asynchronous work tasks interlaced with UI updates, and you need to coordinate IPC between the two.

Using WPF and the traditional Dispatch approach, you might code something like this:

/// <summary>
/// Start a long series of asynchronous tasks using the Dispatcher for coordinating
/// UI updates.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start_Via_Dispatcher_OnClick(object sender, RoutedEventArgs e)
{
 // update initial start time and task status
 Time_Dispatcher.Text = DateTime.Now.ToString("hh:mm:ss");
 Status_Dispatcher.Text = "Started";
 
 // create UI dont event object
 var uiUpdateDone = new ManualResetEvent(false);
 
 // Start a new task (this uses the default TaskScheduler, 
 // so it will run on a ThreadPool thread).
 Task.Factory.StartNew(async () =>
 {
  // We are running on a ThreadPool thread here.
 
  // Do some work.
  await Task.Delay(2000);
 
  // Report progress to the UI.
  Application.Current.Dispatcher.Invoke(() =>
  {
   Time_Dispatcher.Text = DateTime.Now.ToString("hh:mm:ss");
 
   // signal that update is complete
   uiUpdateDone.Set();
  });
 
  // wait for UI thread to complete and reset event object
  uiUpdateDone.WaitOne();
  uiUpdateDone.Reset();
 
  // Do some work.
  await Task.Delay(2000); // Do some work.
 
  // Report progress to the UI.
  Application.Current.Dispatcher.Invoke(() =>
  {
   Time_Dispatcher.Text = DateTime.Now.ToString("hh:mm:ss");
 
   // signal that update is complete
   uiUpdateDone.Set();
  });
 
  // wait for UI thread to complete and reset event object
  uiUpdateDone.WaitOne();
  uiUpdateDone.Reset();
 
  // Do some work.
  await Task.Delay(2000); // Do some work.
 
  // Report progress to the UI.
  Application.Current.Dispatcher.Invoke(() =>
  {
   Time_Dispatcher.Text = DateTime.Now.ToString("hh:mm:ss");
 
   // signal that update is complete
   uiUpdateDone.Set();
  });
 
  // wait for UI thread to complete and reset event object
  uiUpdateDone.WaitOne();
  uiUpdateDone.Reset();
 },
 CancellationToken.None,
 TaskCreationOptions.None,
 TaskScheduler.Default)
  .ConfigureAwait(false)
  .GetAwaiter()
  .GetResult()
  .ContinueWith(_ =>
  {
   Application.Current.Dispatcher.Invoke(() =>
   {
    Status_Dispatcher.Text = "Finished";
 
    // dispose of event object
    uiUpdateDone.Dispose();
   });
  });
}

This code works as intended, but has the following drawbacks:


  1. The code is tied to the WPF Application.Current.Dispatcher object. This makes this difficult to unit test and abstract.
  2. The need for an external ManualResetEvent object for synchronizing between threads. This should immediately set off a code smell, since this now depends on another resource that needs to be mocked.
  3. Difficulty in managing object lifetime for said same kernel object.


Now, lets try this again using the SynchronizationContext object:

/// <summary>
/// Start a long series of asynchronous tasks using the SynchronizationContext object for
/// coordinating UI updates.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start_Via_SynchronizationContext_OnClick(object sender, RoutedEventArgs e)
{
 // update initial time and task status
 Time_SynchronizationContext.Text = DateTime.Now.ToString("hh:mm:ss");
 Status_SynchronizationContext.Text = "Started";
 
 // capture synchronization context
 var sc = SynchronizationContext.Current;
 
 // Start a new task (this uses the default TaskScheduler, 
 // so it will run on a ThreadPool thread).
 Task.Factory.StartNew(async () =>
 {
  // We are running on a ThreadPool thread here.
 
  // Do some work.
  await Task.Delay(2000);
 
  // Report progress to the UI.
  sc.Send(state =>
  {
   Time_SynchronizationContext.Text = DateTime.Now.ToString("hh:mm:ss");
  }, null);
 
  // Do some work.
  await Task.Delay(2000);
 
  // Report progress to the UI.
  sc.Send(state =>
  {
   Time_SynchronizationContext.Text = DateTime.Now.ToString("hh:mm:ss");
  }, null);
 
  // Do some work.
  await Task.Delay(2000);
 
  // Report progress to the UI.
  sc.Send(state =>
  {
   Time_SynchronizationContext.Text = DateTime.Now.ToString("hh:mm:ss");
  }, null);
 },
 CancellationToken.None,
 TaskCreationOptions.None,
 TaskScheduler.Default)
 .ConfigureAwait(false)
 .GetAwaiter()
 .GetResult()
 .ContinueWith(_ =>
 {
  sc.Post(state =>
  {
   Status_SynchronizationContext.Text = "Finished";
  }, null);
 });
}

Notice this time through, we dont need to rely on external objects for synchronizing between threads. We are, in fact, synchronizing between contexts.

Now, even though you didn't ask, but for the sake of completeness, there is one more way to accomplish what you want in an abstracted way without the need for the SynchronizationContext
 object or using the Dispatcher. Since we are already using the TPL (Task Parallel Library) for our task handling, we could just use the task scheduler as follows:

/// <summary>
/// Start a long series of asynchronous tasks using the task scheduler object for
/// coordinating UI updates.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start_Via_TaskScheduler_OnClick(object sender, RoutedEventArgs e)
{
 Time_TaskScheduler.Text = DateTime.Now.ToString("hh:mm:ss");
 
 
 // This TaskScheduler captures SynchronizationContext.Current.
 var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
 Status_TaskScheduler.Text = "Started";
 
 // Start a new task (this uses the default TaskScheduler, 
 // so it will run on a ThreadPool thread).
 Task.Factory.StartNew(async () =>
 {
  // We are running on a ThreadPool thread here.
 
  // Do some work.
  await Task.Delay(2000);
 
  // Report progress to the UI.
  var reportProgressTask = ReportProgressTask(taskScheduler, () =>
  {
   Time_TaskScheduler.Text = DateTime.Now.ToString("hh:mm:ss");
   return 90;
  });
 
  // get result from UI thread
  var result = reportProgressTask.Result;
  Debug.WriteLine(result);
 
  // Do some work.
  await Task.Delay(2000); // Do some work.
 
  // Report progress to the UI.
  reportProgressTask = ReportProgressTask(taskScheduler, () =>
   {
    Time_TaskScheduler.Text = DateTime.Now.ToString("hh:mm:ss");
    return 10;
   });
 
  // get result from UI thread
  result = reportProgressTask.Result;
  Debug.WriteLine(result);
 
  // Do some work.
  await Task.Delay(2000); // Do some work.
 
  // Report progress to the UI.
  reportProgressTask = ReportProgressTask(taskScheduler, () =>
  {
   Time_TaskScheduler.Text = DateTime.Now.ToString("hh:mm:ss");
   return 340;
  });
 
  // get result from UI thread
  result = reportProgressTask.Result;
  Debug.WriteLine(result);
 }, 
 CancellationToken.None,
 TaskCreationOptions.None,
 TaskScheduler.Default)
  .ConfigureAwait(false)
  .GetAwaiter()
  .GetResult()
  .ContinueWith(_ =>
  {
   var reportProgressTask = ReportProgressTask(taskScheduler, () =>
   {
    Status_TaskScheduler.Text = "Finished";
    return 0;
   });
   reportProgressTask.Wait();
  });
}
 
/// <summary>
/// Helper for creating a UI update task
/// </summary>
/// <param name="taskScheduler"></param>
/// <param name="func"></param>
/// <returns></returns>
private Task<int> ReportProgressTask(TaskScheduler taskScheduler, Func<int> func)
{
 var reportProgressTask = Task.Factory.StartNew(func,
  CancellationToken.None,
  TaskCreationOptions.None,
  taskScheduler);
 return reportProgressTask;
}

As they say, there is more than one way to schedule as task ;)

Sample source (AsyncAwait) can be found in my Git repository here:
SampleSource

Comments

Popular posts from this blog

Implementing the try/catch/finally pattern in C++ using lambdas, or why cant this be as easy as it is in C#?

Representing C/C++ unions and bitfields in C#

Debugging isn't magic! Patience and common sense goes a long way