C++/WinRT envy: Bringing thread switching tasks to C# (UWP edition)

Raymond Chen

Raymond

Last time, we developed a Run­Task­Async method to try to make it easier to switch threads in a task, but we saw that while it simplified some operations, it was still cumbersome because of the difficulty of sharing state between the main method and the async lambdas that it kicked off to other threads.

Let’s fix that by stealing an idea from C++/WinRT: Make thread-switching an awaitable operation.

In C++/WinRT, you can switch threads by awaiting a magic object where you enter on one thread and pop out the other side on a different thread. It’s like Portal for threads!

// C++/WinRT

winrt::fire_and_forget MyPage::Button_Click()
{
  // We start on a UI thread.
  auto lifetime = get_strong();

  // Get the control's value from the UI thread.
  auto v = SomeControl().Value();

  // Move to a background thread.
  co_await winrt::resume_background();

  // Do the computation on a background thread.
  auto result1 = Compute1(v);
  auto other = co_await ContactWebServiceAsync();
  auto result2 = Compute2(result1, other);

  // Return to the UI thread to provide an interim update.
  co_await winrt::resume_foreground(Dispatcher());

  // Back on the UI thread: We can update UI elements.
  TextBlock1().Text(result1);
  TextBlock2().Text(result2);

  // Back to the background thread to do more computations.
  co_await winrt::resume_background();

  auto extra = co_await GetExtraDataAsync();
  auto result3 = Compute3(result1, result2, extra);

  // Return to the UI thread to provide a final update.
  co_await winrt::resume_foreground(Dispatcher());

  // Update the UI one last time.
  TextBlock3().Text(result3);
}

The thread-switching is expressed simply as an asynchronous operation. Constructors and destructors still run at the usual times, so you can use RAII types naturally. You can perform these magic co_await operations inside loops or conditionals, and they behave in the natural way.

// Move to a background thread if a condition is met.
if (condition) {
  co_await winrt::resume_background();
}

DoSomething();

In the above case, the Do­Something() occurs on a background thread if the condition is met, or it occurs on the current thread if the condition is not met. This sort of flexibility is difficult to express using our previous model of always putting off-thread actions into an asynchronous lambda.

Okay, enough hype. Let’s bring resume_foreground() and resume_background() to C#!

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using Windows.System.Threading;
using Windows.UI.Core;

struct DispatcherThreadSwitcher : INotifyCompletion
{
    internal DispatcherThreadSwitcher(CoreDispatcher dispatcher) =>
        this.dispatcher = dispatcher;
    public DispatcherThreadSwitcher GetAwaiter() => this;
    public bool IsCompleted => dispatcher.HasThreadAccess;
    public void GetResult() { }
    public void OnCompleted(Action continuation) =>
        _ = dispatcher.RunAsync(CoreDispatcherPriority.Normal,
                                () => continuation());
    CoreDispatcher dispatcher;
}

struct ThreadPoolThreadSwitcher : INotifyCompletion
{
    public ThreadPoolThreadSwitcher GetAwaiter() => this;
    public bool IsCompleted =>
       SynchronizationContext.Current == null;
    public void GetResult() { }
    public void OnCompleted(Action continuation) =>
        _ = ThreadPool.RunAsync(_ => continuation());
}

class ThreadSwitcher
{
    static public DispatcherThreadSwitcher ResumeForegroundAsync(
        CoreDispatcher dispatcher) =>
        new DispatcherThreadSwitcher(dispatcher);
    static public ThreadPoolThreadSwitcher ResumeBackgroundAsync() =>
        new ThreadPoolThreadSwitcher();
}

We can use the methods in Thread­Switcher the same way we did in C++/WinRT:

public sealed partial class MyPage : Page
{
  void Button_Click()
  {
    // Get the control's value from the UI thread.
    var v = SomeControl.Value;

    // Move to a background thread.
    await ThreadSwitcher.ResumeBackgroundAsync();

    // Do the computation on a background thread.
    var result1 = Compute1(v);
    var other = await ContactWebServiceAsync();
    var result2 = Compute2(result1, other);

    // Return to the UI thread to provide an interim update.
    await ThreadSwitcher.ResumeForegroundAsync(Dispatcher);

    // Back on the UI thread: We can update UI elements.
    TextBlock1.Text = result1;
    TextBlock2.Text = result2;

    // Back to the background thread to do more computations.
    await ThreadSwitcher.ResumeBackgroundAsync();

    var extra = await GetExtraDataAsync();
    var result3 = Compute3(result1, result2, extra);

    // Return to the UI thread to provide a final update.
    await ThreadSwitcher.ResumeForegroundAsync(Dispatcher);

    // Update the UI one last time.
    TextBlock3.Text = result3;
  }
}

This is identical to our original “all on the UI thread” code, excepty for the calls to Thread­Switcher members.

How does this Thread­Switcher class work?

We need to understand the awaitable-awaiter pattern and how the compiler uses it. Read the linked articles for details. In summary, the line

result = await x;

compiles to something spiritually similar to the following:¹

var awaiter = x.GetAwaiter();
if (!awaiter.IsCompleted) {
 awaiter.OnCompleted(() => goto resume);
 return task;
resume:;
}
result = awaiter.GetResult();

First, the compiler calls the Get­Awaiter method to obtain an “awaiter”. If the awaiter says that the task has not yet completed, then the compiler tells the awaiter, “Okay, well, when it’s complete, let me know.” Then the function returns. When the operation finally completes, execution resumes.

When the operation is complete, either because it was complete all along, or because we were resumed after a delayed completion, the result is obtained by calling the awaiter’s Get­Result() method.

You can create custom awaitable things by plugging into the above pattern.

In our case, Thread­Switcher.Resume­Foreground­Async() works as follows:

  • It creates a Dispatcher­Thread­Switcher with the dispatcher you want to use.
  • The Dispatcher­Thread­Switcher.Get­Awaiter method returns itself. The object serves double-duty as the awaitable object and its own awaiter.
  • To determine whether the operation has already completed, the Is­Completed property reports whether we are already on the dispatcher’s thread. If so, then the compiler won’t bother scheduling a continuation; it’ll just keep executing.
  • If we report that the operation has not completed, the compiler will use the On­Completed method to ask us to complete the operation and then call a specific delegate once it’s done. We queue a work item onto the dispatcher’s thread.
  • The work item runs on the dispatcher’s thread, and from that work item, we invoke the completion delegate. The coroutine resumes execution on the dispatcher’s thread, as desired.

The Thread­Switcher.Resume­Background­Async() method works almost the same way, but for the thread pool rather than for a dispatcher.

  • It creates a Thread­Pool­Thread­Switcher.
  • The Thread­Pool­Thread­Switcher.Get­Awaiter method returns itself. Again, the object serves double-duty as the awaitable object and its own awaiter.
  • To determine whether the operation has already completed, we check the current Synchronization­Context. A value of null means that we are already on a background thread.
  • If we report that the operation has not completed, the compiler will use the On­Completed method to ask us to complete the operation and then call a specific delegate once it’s done. We queue a work item onto the thread pool.
  • The work item runs on a thread pool thread, and from that work item, we invoke the completion delegate. The coroutine resumes execution on a thread pool thread, as desired.

All the magic is done by a handful of one-line methods.

Integrating thread switching via await not only simplifies the code, it also opens up new usage patterns that were difficult to accomplish without it.

// Assume we enter on the UI thread.
using (var connection = new Connection()) {

    // Initialize on the UI thread since
    // we need information from UI objects.
    connection.Initialize(SomeParameter);

    await ThreadSwitcher.ResumeBackgroundAsync();
    // Execute on a background thread.
    connection.Execute();

} // connection is disposed here

// Process the results on a background thread.
Process(connection.GetResults());

Notice that we switched threads right in the middle of the using block, so that we exited the block on a different thread from the one we started!

When I show this trick to people, their reactions tend to fall into one of two categories.

  1. This is truly embracing the concept of asynchronous operations, and it’s a game-changer for code that needs to perform multiple actions on different threads. We should make this trick more widely known.
  2. This is an offense against nature. C# developers have long internalized the rule that “Unless explicitly configured, await does not switch threads,” but this class violates that rule.

Let me know in the comments which side you identify with. And if you identify with the first group, should I adopt the Thread­Switcher class in the UWP samples repo?

Next time, we’ll implement the Thread­Switcher methods for WPF and WinForms.

¹ In reality, the compiler remembers where to resume execution in a state variable prior to the return, and the goto resume is done by resuming the state machine.

Raymond Chen
Raymond Chen

Follow Raymond   

7 Comments
Avatar
Sunil Joshi 2019-03-28 08:22:46
The first time I saw this demonstrated by Kenny Kerr for C++ / WinRT, I felt actual joy. Not only should this be more widely known but it should be added to the Framework libraries. In the meantime, you should use it in the samples!
Avatar
Joel Phelps 2019-03-28 09:15:31
This method of handling thread switching/async is definitely a game changer and a useful tool to have in the developer toolbox. I do see the concerns though.  It is not really obvious that you are switching threads.  As a .NET developer, I have learned to be conscious of needing to update on the UI thread, as well as handling synchronization back to the UI thread.  Thus this code does "look" wrong.  So unless you know the trick it looks like a bug. I also have concerns regarding how code like this will work in unit tests.
Avatar
Pete D 2019-03-28 10:04:35
"Let me know in the comments which side you identify with" -- neither. IMHO, it's a mistake for a C# developer to believe that "await" always remains on the same thread unless configured otherwise. Granted, it's a feature of the default implementation, and in the, by far, most common UI-related scenarios where "await" is used, that is indeed what happens. But as the article above demonstrates clearly, what thread the continuation executes on is implementation-dependent, even in absence of calling "ConfigureAwait()" (which is really just saying "I don't care where the continuation happens"). But on the other hand, "game changer"? The number of scenarios where execution of some logic *must* start on one thread, and then *must* continue in a different thread, very few. For those few scenarios, there are previously-existing mechanisms to accomplish it that have worked fine for years. Being able to apply the C# "async/await" pattern to those scenarios is a very nice convenience, but it's no more a "game changer" than the "async/await" pattern is for UI-related scenarios (which people, including myself, have been getting along just fine without for some two decades...it was never really that hard to deal with switching back to the UI thread, especially in .NET). So, no...not a "game changer" either.
Avatar
Christoffer Haglund 2019-03-28 10:33:23

Would it make sense to declare a [ThreadStatic] thread-switcher as well? In a view, like the example above, you have the Dispatcher property very easily accessible, but in a model or view model (assuming you follow the MVVM pattern) you'd need to either pass it in or get it through Window.Current or similar lookup. While not particularly difficult, perhaps it would make view models look even cleaner if we could do something like

void MethodCalledFromView()
{ var foregroundThreadContext = ThreadSwitcher.GetCurrent(); await ThreadSwitcher.ResumeBackgroundAsync(); // do work await foregroundThreadContext; // update properties and post property change events }

Another approach would be to let the helper methods return previous context, that would enable a pattern like

void MethodCalledFromView()
{ var foregroundContext = await ThreadSwitcher.ResumeBackgroundAsync(); // do work in background await foregroundContext; // do work on the original thread }
Avatar
Nick Allmaker 2019-03-28 10:44:14
I love this
Avatar
Nick . 2019-03-28 13:05:19
I like this quite a bit, especially how linear and readable it is (usually the opposite being true for thread-related activities).  Using "await" just to switch thread contexts does feel a little dirty, but if it's a common pattern in the application then that's probably not a big deal. The awaitable pattern in C# is pretty neat, and one of the rare examples of duck typing in the language.  I admit being a little surprised that GetResult() can be void, but I guess that's necessary to support async void methods.
Avatar
Steven Wolf 2019-03-28 15:26:09
This is brilliant!  Being able to schedule things on the UI thread simply by using a single line of code w/o having to worry about stack context and ownership etc. except in terms of the function itself?!  YES!  I'll take a dozen, please.
Avatar
Ji Luo 2019-03-28 16:32:44
The sugar’s so sweet! Just being curious: do the destructors run on a different thread if we’re on a different thread when the control goes beyond the closing brace?
Avatar
Aybe One 2019-03-28 17:55:59
This looks very interesting but I am having a hard time understanding where the magic is VS when you use Dispatcher.BeginInvoke? Example:
async Task<Whatever> DoSomethingLongAsync()
{
    var stuff = await Dispatcher.BeginInvoke(...); // get something requiring UI thread
    // aren't we at this point, back to our thread already ?
    var result = ... // do something with 'stuff'
    return result;
}
Co-incidentally, you wrote that post while I have an issue in my Unity3D game where long loading times freezes it since I need to use its main thread. I came with the idea of a Dispatcher-alike system but now that you've dropped what appears to be a gem according to comments, I am wondering whether it'd be nice to have, until I understand its benefits. Thanks!
Avatar
Fred Miller 2019-03-29 03:20:17
That "unless explicitly configured, `await` does not switch threads" simply isn't true.  Consider a console app without a `TaskScheduler` or `SynchronizationContext`.  An `await someTask;` there will switch to a thread pool thread.
Avatar
Fred Miller 2019-03-29 03:48:51
Async/await could have been designed to be a simple syntactic wrapper around reset/shift which has long been there in functional languages.  I see it unfortunate that the .NET implementation of async/await enforces that the awaited thing always run (no matter where and when) its received continuation once and only once, by giving up control over the async method's control flow to the state machine (the state machine's `MoveNext` method does not accept a parameter specifying where it should move next; it always picks up where it lefted).  That way async/await has become truly a tool unusable anywhere except in asynchnorous programming contexts.  Otherwise, C# could have had the imperative counterpart to LINQ (consider `await RunEverythingBelowThisManyTimes(10); ThisWillBeExecutedTenTimes();`).
Avatar
毅 吕 2019-03-29 17:46:13
Hi Raymond Chen, I've written a WPF version of this kind of thread switching. See here: https://blog.walterlv.com/post/bring-thread-switching-tasks-to-csharp-for-wpf.html But it is written in Chinese. It works correctly.
Avatar
Gunnar Dalsnes 2019-03-30 13:46:49
Im confused about the  SynchronizationContext.Current check.What if I set this to null on the gui thread?And what if suddenly not null on a background thread?