Exploring the async/await State Machine – Synchronization Context

Intro

In this article, I’ll start exploring some of the most popular questions related to async/await.

Concretely, in the next couple of posts, I’ll clarify the misconceptions around the Synchronization Context and the usage of ConfigureAwait in nested and sequential async calls.

First, it’s essential to understand why and when we need the Synchronization Context and the role it plays in the async/await workflow.

Let’s dive in!

This article’s content is influenced by the async/await material in the book C# in Depth by Jon Skeet and the blog posts ConfigureAwait FAQ (Stephen Toub) and Dissecting the async methods in C# (Sergey Tepliakov).

For additional in-depth materials on async/await in C#, you can check this Pluralsight course.

In What “Context” Do Continuations Run?

From the article on the Awaitable Pattern you know that awaitable types, like Task<T>, execute continuations when the async operation completes.

One topic I did not discuss, though, is related to the context (I’ll clarify what context means) in which the continuations run:

What Do I Mean By “Context”?

One simple yet limiting way to think about the context is the exact thread the continuation runs on.

In some cases, this way of thinking is adequate as some environments have concrete requirements regarding the thread to execute the code. One example is UI apps where, as you know, any code that touches the UI should run on the UI thread (more on this later in the article).

However, the notion of context is more general than that.

Some environments don’t care about the exact thread but still have some other constraints. For example, the XUnit framework limits the degree of concurrency by running continuations on a limited number of dedicated threads.

Most environments don’t have any specific requirements about the context. In those cases, the continuations execute on some thread from the Thread Pool. This is achieved via the default Task Scheduler, which is out of scope for the current discussion.

In this article, I’m focusing on the special contexts and how they get implemented.

This leads the discussion towards the Synchronization Context.

Synchronization Context – Overview

The Synchronization Context is just a type that allows for scheduling a piece of code for asynchronous execution. This is achieved by passing a callback to a virtual Post method.

It’s a lot easier to understand the concept by looking at some concrete implementations.

Let’s do that next.

The Default Synchronization Context

The default Synchronization Context implementation would just schedule your callback to the Thread Pool:

public virtual void Post(SendOrPostCallback d, Object state)
{
   ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}

The exciting part comes when you need something more complex for your use case. In those scenarios, you need to create your own Synchronization Context inheriting from the default one.

Let’s see a few examples.

Context-Specific Environments – Examples

We’ve mentioned UI apps where only the UI thread can interact with the UI components.

Let’s see how this is achieved when dealing with asynchronous calls.

Have a look at the following button click handler:

private async void btn1_Click(object sender, EventArgs e)
{
    await Task.Delay(1000);
    myControl.text = “After Await”;
}

We know that at the await point on line 3, the method will pause, and attach a continuation to the task awaiter. This continuation modifies a UI control (Line 4) which means that Line 4 should run on the UI thread.

So, the question is:

How do UI apps guarantee that async continuations run on the UI thread?

The answer is simple – they implement their own Synchronization Context with the proper UI access rules.

For example, the WindowsFormsSynchronizationContext calls BeginInvoke in its’ Post method on the control to dispatch the call to the UI thread.

WPF has its own SynchronizationContext-derived type calling BeginInvoke on the Dispatcher.

I’ve already mentioned XUnit and its’ restrictions on the degree of parallelism. You can explore the Synchronization Context implementation here.

How To Set/Get The Current Synchronization Context?

For setting and retrieving the current SynchronizationContext, you can use the static SetSynchronizationContext method and the Current property.

Please spend a moment on this quick example demonstrating how to set and get the current Synchronization Context:

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

namespace GetSetContext
{
    internal static class Program
    {
        public static async Task Main(string[] args)
        {   
            Console.WriteLine($"Current Synchronization Context: {SynchronizationContext.Current}");
            
            SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext());
            
            Console.WriteLine($"Current Synchronization Context: {SynchronizationContext.Current}");
            
            await Task.Delay(100);
            
            Console.WriteLine("Completed!");
        }

        private class MySynchronizationContext : SynchronizationContext
        {
            public override void Post(SendOrPostCallback d, object state)
            {
                Console.WriteLine($"Continuation dispatched to {nameof(MySynchronizationContext)}");
                d.Invoke(state);
            }
        }
    }
}

This piece of code outputs the following:

Current Synchronization Context: null
Current Synchronization Context: MySynchronizationContext
Continuation dispatched to MySynchronizationContext
Completed!

Note that this is a simplified implementation. Typically, it requires some more housekeeping.

For instance, you have to preserve the previous Synchronization Context and post back to it after your own async execution completes.

Here is a great article where Stephen Toub builds a single-threaded context for console apps.

Where Does It Fit Into the Awaitable Pattern

At this point, you know that if you set your own Synchronization Context, it will start processing the async continuations.

One interesting question is what piece of code invokes your Synchronization Context? Where does it happen in the async infrastructure?

If you’ve read the Awatable Pattern article, you may already suspect that this happens somewhere as part of the OnCompleted method execution:

Let’s see the details.

Capturing the Synchronization Context

The actual implementation is quite involved so let’s just see some of the code for a better intuition.

Below is the essence of the logic that captures the Synchronization Context:

[SecurityCritical]
internal void SetContinuationForAwait(
    Action continuationAction, bool continueOnCapturedContext, bool flowExecutionContext, ref StackCrawlMark stackMark)
{
    TaskContinuation tc = null;
    
    if (continueOnCapturedContext)
    {
        var syncCtx = SynchronizationContext.Current;
        
        // If there is a Synchronization Context and it is not the default one 
        if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))
        {
            // Capture the Synchronization Context
            tc = new SynchronizationContextAwaitTaskContinuation(syncCtx, continuationAction, flowExecutionContext, ref stackMark);
        }
    }
    
    // ...
}

This piece of code is deep into the async infrastructure, and I have also simplified it a bit.

Basically, the runtime will capture the current Synchronization Context and post the continuations to it if:

  1. We haven’t explicitly instructed the task to forget about the current Synchronization Context by calling ConfigureAwait(false). This is stored in the variable continueOnCapturedContext (Line 7).
  2. There is a current Synchronization Context that is different from the default one (Line 12).

I hope this gives at least a little intuition of how and when the Synchronization Context is captured and propagated through the async execution.

Summary

In this article, you’ve explored the concept of Synchronization Context and its’ implementations in different context-specific frameworks.

You’ve also seen how you can create and assign your own Synchronization Context.

Moreover, we’ve dug deeper into the async infrastructure codebase to understand when and how the Synchronization Context is captured and propagated.

Armed with the knowledge so far, I’ll move on with this series by answering some of the most common async/await questions related to nested async calls and the usage of ConfigureAwait.

Stay tuned, and thanks for reading!

Resources

  1. C# in Depth: Fourth Edition
  2. Dissecting the async methods in C#
  3. ConfigureAwait FAQ

Site Footer

Subscribe To My Newsletter

Email address