Explicitly switch to the UI thread in an async method

Very poorPoorAverageGoodExcellent (1 votes) 
Loading...

Async code is a great way to keep your app’s UI responsive. You can start an async operation from the UI thread, await it without blocking the UI thread, and naturally resume on the UI thread when it’s done. This is a very powerful feature, and most of the time you don’t even need to think about it; it “just works”. However, this works only if the async operation is started from a thread that has a synchronization context (such as the UI thread in Windows Forms, WPF or WinRT). If you don’t have a sync context when the async operation starts, or if resuming on the sync context is explicitly disabled with ConfigureAwait(false), then the method resumes on a thread pool thread after the await, and there is no obvious way to get back to the UI thread.

For instance, let’s assume you want to do something like this:

public async void btnStart_Click(object sender, RoutedEventArgs e)
{
    lblStatus.Text = "Working...";
    
    // non blocking call
    var data = await GetDataFromRemoteServerAsync().ConfigureAwait(false);
    // blocking call, but runs on a worker thread
    DoSomeCpuBoundWorkWithTheData(data);
    
    // Oops, not on the UI thread!
    lblStatus.Text = "Done";
}

This method starts an async operation to retrieve some data, and doesn’t resume on the UI thread, because it has some work to do in the background. When it’s done, it tries to update the UI, but since it’s not on the UI thread, it fails. This is a well known issue, and there are several workarounds. The most obvious one is to explicitly marshall the action to the dispatcher thread:

Dispatcher.Invoke(new Action(() => lblStatus.Text = "Done"));

But it’s not very elegant and readable. A better way to do it is simply to extract the part of the method that does the actual work to another method:

public async void btnStart_Click(object sender, RoutedEventArgs e)
{
    lblStatus.Text = "Working...";
    await DoSomeWorkAsync();
    lblStatus.Text = "Done";
}

private async Task DoSomeWorkAsync()
{
    // non blocking call
    var data = await GetDataFromRemoteServerAsync().ConfigureAwait(false);
    // blocking call, but runs on a worker thread
    DoSomeCpuBoundWorkWithTheData(data);
}

It takes advantage of the normal behavior of await, which is to resume on the captured sync context if it was available.

However, there might be some cases where it’s not practical to split the method like this. Or perhaps you want to switch to the UI thread in a method that didn’t start on the UI thread. Of course, you can use Dispatcher.Invoke, but it doesn’t look very nice with all those lambda expressions. What would be nice would be to be able to write something like this:

await syncContext;

Well, it’s actually pretty simple to do: we just need to create a custom awaiter. The async/await feature is pattern-based; for something to be “awaitable”, it must:

  • have a GetAwaiter() method (which can be an extension method), which returns an object that:
    • implements the INotifyCompletion interface
    • has an IsCompleted boolean property
    • has a GetResult() method that synchronously returns the result (or void if there is no result)

So, we need to create an awaiter that captures a synchronization context, and causes the awaiting method to resume on this synchronization context. Here’s how it looks like:

public struct SynchronizationContextAwaiter : INotifyCompletion
{
    private static readonly SendOrPostCallback _postCallback = state => ((Action)state)();

    private readonly SynchronizationContext _context;
    public SynchronizationContextAwaiter(SynchronizationContext context)
    {
        _context = context;
    }

    public bool IsCompleted => _context == SynchronizationContext.Current;

    public void OnCompleted(Action continuation) => _context.Post(_postCallback, continuation);

    public void GetResult() { }
}
  • The constructor takes the synchronization context that the continuation needs to resume on.
  • The IsCompleted property returns true only if we’re already on this synchronization context.
  • OnCompleted is called only if IsCompleted was false. It accepts a “continuation”, i.e. that code that must be executed when the operation completes. In an async method, the continuation is just the code that is after the awaited call. Since we want that continuation to run on the UI thread, we just post it to the sync context.
  • GetResult() doesn’t need to do anything; it will just be called as part of the continuation.

Now that we have this awaiter, we just need to create a GetAwaiter extension method for the synchronization context that returns this awaiter:

public static SynchronizationContextAwaiter GetAwaiter(this SynchronizationContext context)
{
    return new SynchronizationContextAwaiter(context);
}

And we’re done!

The original method can now be rewritten like this:

public async void btnStart_Click(object sender, RoutedEventArgs e)
{
    var syncContext = SynchronizationContext.Current;
    lblStatus.Text = "Working...";
    
    // non blocking call
    var data = await GetDataFromRemoteServerAsync().ConfigureAwait(false);
    // blocking call, but runs on a worker thread
    DoSomeCpuBoundWorkWithTheData(data);
    
    // switch back to the UI thread
    await syncContext;
    lblStatus.Text = "Done";
}

4 Comments

  1. While this is a nice trick, I don’t recommend it for production code. There’s always a better solution for switching to the UI thread context (await, Progress, Rx’s ObserveOn).

    Notes:

    1) In the general case, it’s wrong to assume that code after a ConfigureAwait(false) is going to run on a thread pool thread.

    Specifically, if GetDataFromRemoteServerAsync completes very quickly, then DoSomeCpuBoundWorkWithTheData will run on the UI thread. This is possible (though unlikely) to happen if the remote server is actually contacted over the network. But what if GetDataFromRemoteServerAsync was refactored to have an in-memory cache? Or what if the remote server call is a GET and an OS update adds more aggressive HTTP caching (this is a real scenario on Windows Phone that catches many devs by surprise)? In those cases, the DoSomeCpuBoundWorkWithTheData will suddenly and unexpectedly run on the UI thread.

    IMO, it’s better to explicitly send work to a background thread if you need it to run on a background thread. The code has clearer intent and it avoids that nasty race condition:

    lblStatus.Text = “Working…”;
    var data = await GetDataFromRemoteServerAsync();
    await Task.Run(() => DoSomeCpuBoundWorkWithTheData(data));
    lblStatus.Text = “Done”;

    2) The original Async CTP did have “SwitchTo” extensions so you can use await to switch contexts, very similar to what you have in this article. However, they were removed before RTM because they can be easily misused.

    Specifically, they become tricky when doing exception handling:

    try
    {
    var syncContext = SynchronizationContext.Current;
    DoUiStuff();
    var data = await GetDataFromRemoteServerAsync().ConfigureAwait(false);
    DoSomeCpuBoundWorkWithTheData(data);
    await syncContext;
    DoMoreUiStuff();
    }
    catch
    {
    // Wait, am I on the UI thread or not?
    // Hopefully all my Dispose methods are threadsafe…
    }

    • Thomas Levesque says:

      Hi Stephen,

      Thanks for your comment! Your async expertise is very appreciated 😉

      You’re right, I forgot that GetDataFromRemoteServerAsync could return synchronously… And as you said, it’s better to be explicit when offloading work to another thread.

      I guess I got a little too excited about my idea, and didn’t really think it through… Well, at least I learned something about custom awaiters 😉

  2. Joshua A. Schaeffer says:

    “I don’t recommend it for production code.” Well, I do. I can and have coded circles around others using the pattern and can scale to anything. I think they removed the methods only because there is no single unambiguous use case.

    I have evolved it further though; in my MVVM code I may have multiple UI threads that may terminate at any time, so my switch code accepts a CancellationToken. Optionally, the switching code is inside a using() statement that refcounts the UI thread from exiting. In WPF I more explicitly support Dispatcher, so I also take a DispatcherPriority which is hugely important when pumping data into a live list.

1 Trackbacks

Leave a comment

css.php