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   

20 comments

Comments are closed.

  • Avatar
    Sunil Joshi

    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

    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
    Christoffer Haglund

    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 .

    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

    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

    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

    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!

    • Raymond Chen
      Raymond Chen

      Well, Dispatcher.BeginInvoke() isn’t awaitable, so let’s await on the DispatcherOperation.Task instead. But now you have the same problems we saw over the past two days. The task completes before your ... has completed, and any values you want to provide back to the UI thread have to be hoisted out of the lambda. And then there’s a third problem: An async lambda is not implicitly convertible to Delegate. You’ll have to manually cast it to Func<Task>, which is even more cumbersome. Just to get the code to compile, you’ll have to do this:

      async Task DoSomethingLongAsync()
      {
          string result1 = null;
          string result2 = null;
          await Dispatcher.BeginInvoke((Func<Task>)(async () => {
              result1 = Compute1();
              var other = await ContactWebServiceAsync();
              result2 = Compute2(result1, other);
          })).Task;
          TextBlock1.Text = result1;
          TextBlock2.Text = result2; // oops, this displays nothing
      }
      

      and you still have to solve the problem that the TextBlock2.Text = result2 displays nothing because result2 is null.

  • Avatar
    Fred Miller

    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.

    • Raymond Chen
      Raymond Chen

      More precisely, it would be “unless explicitly configured, ‘await’ does not switch synchronization contexts”. But each UI thread has its own synchronization context, so it’s convenient (although not precise) to think of synchronization contexts as UI threads.

      • Avatar
        Fred Miller

        Or perhaps even more precisely — “`await`ing `Task`s and other .NET standard library awaitable objects, except `ConfiguredTaskAwaitable`, does not switch synchronization contexts.”  I’m not sure if the language guideline dictates that implementors of awaitable types respect `SynchronizationContext.Current`; .NET classes follow this convention, but ultimately this is not enforced by the compiler or runtime, it is safe to break this convention, and here it becomes obvious that doing so is absolutely helpful in many asynchronous programming problems.

  • Avatar
    Fred Miller

    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();`).

  • Gunnar Dalsnes
    Gunnar Dalsnes

    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?

    • Avatar
      Kalle Niemitalo

      If SynchronizationContext.Current == null on a GUI thread, you will have other problems.  Don’t do that.
      SynchronizationContext.Current != null on a thread-pool thread might happen, e.g. if someone reads AsyncOperationManager.SynchronizationContext.  To handle that, ThreadPoolThreadSwitcher.IsCompleted should perhaps also check SynchronizationContext.Current.GetType() == typeof(SynchronizationContext), like e.g. YieldAwaitable.YieldAwaiter.QueueContinuation does.  However, I don’t think omitting this check causes any real problem.  ThreadPoolThreadSwitcher will just unnecessarily enqueue another task on the thread pool instead of running the code on the current thread.