Await, SynchronizationContext, and Console Apps: Part 2

Stephen Toub - MSFT

Yesterday, I blogged about how you can implement a custom SynchronizationContext in order to pump the continuations used by async methods so that they may be processed on a single, dedicated thread.  I also highlighted that this is basically what UI frameworks like Windows Forms and Windows Presentation Foundation do with their message pumps.

Now that we understand the mechanics of how these things work, it’s worth pointing out that we can achieve the same basic semantics without writing our own custom SynchronizationContext.  Instead, we can use one that already exists in the .NET Framework: DispatcherSynchronizationContext.  Through its Dispatcher class and its PushFrame method, WPF provides the ability to (as described in MSDN) “enter an execute loop” that “processes pending work items.”  This is exactly what our custom SynchronizationContext was doing with its usage of BlockingCollection<T>, so we can just use WPF’s support instead of developing our own. (You might ask then why I started by describing how to do it manually and writing my own to exemplify it.  I do so because I think it’s important to really understand how things work; I typically find that developers write better higher-level code if they have the right mental model for what’s happening under the covers, allowing them to better reason about bugs, about performance, about reliability, and the like.)

Below you can see how few lines of code it takes to achieve this support.  (To compile this code, you’ll need to reference WindowsBase.dll to bring in the relevant WPF types.)

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;

public static class AsyncPump
{
    public static void Run(Func<Task> func)
    {
        if (func == null) throw new ArgumentNullException(“func”);

        var prevCtx = SynchronizationContext.Current;
        try
        {
            var syncCtx = new DispatcherSynchronizationContext();
            SynchronizationContext.SetSynchronizationContext(syncCtx);

            var t = func();
            if (t == null) throw new InvalidOperationException();

            var frame = new DispatcherFrame();
            t.ContinueWith(_ => { frame.Continue = false; },
                TaskScheduler.Default);
            Dispatcher.PushFrame(frame);

            t.GetAwaiter().GetResult();
        }
        finally
        { 
            SynchronizationContext.SetSynchronizationContext(prevCtx);
        }
    }
}

In short, we instantiate a DispatcherSynchronizationContext and publish it to be Current on the current thread; this is what enables the awaits inside of the async method being processed to queue their continuations back to this thread and this thread’s Dispatcher.  Then we instantiate a DispatcherFrame, which represents an execute loop for WPF’s message pump.  We use a continuation to signal to that DispatcherFrame when it should exit its loop (i.e. when the Task completes).  And then we start the frame’s execute loop via the PushFrame method.

All in all, just a few lines of code to achieve some powerful and useful behavior.

0 comments

Discussion is closed.

Feedback usabilla icon