While the differences have been pointed out, I don't really see that reason for choosing one over another explicitly spelled out here. So perhaps it would help to explain what problem the SynchronizationContext
object is trying to solve in the first place:
- It provides a way to queue a unit of work to a context. Notice that this isn't thread specific, and so we avoid the problem of thread affinity.
- Every thread has a "current" context, but that context might be shared across threads i.e. a context is not necessarily unique.
- Contexts keep a count of outstanding asynchronous operations. This count is often, but not always incremented/decremented on capture/queuing.
So to answer your question of which one to choose, it would seem just from the criteria above that using the SynchronizationContext
would be preferable to the Dispatcher
.
But there are even more compelling reasons to do so:
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:
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 and Dependency Injection
As you have already seen, the SynchronizationContext
is implemented across many UI frameworks: WinForms, WPF, ASP.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.
You don't need to even inject a context object... you can inject any object with an interface that matches the methods on the context object, including proxies.
By way of example:
Note: I 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:
- The code is tied to the WPF
Application
Dispatcher
object. This makes this difficult to unit test and abstract.
- 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.
- Difficulty in managing object lifetime for said same kernel object.
Now, lets try this again using the SynchronizationContext
object:
/// <summary>
///
/// </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>
///
/// </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>
///
/// </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 a task ; )