Asynchronous programming async/await syntactic sugar: SynchronizationContext

1. Deadlock site caused by async/await

1.1 UI example.

Clicking a button triggers an HTTP request to modify a UI control with the return value of the request, the following code triggers a deadlock (similar state occurs in WinForm, WPF)

public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}

// Upper level call method
public void Button1_Click(...)
{
  var jsonTask = GetJsonAsync(...);
  textBox1.Text = jsonTask.Result;
}

1.2 ASP.NET web example

Launching a remote HTTP request from the api and waiting for the result of the request, the following code will also trigger a deadlock

public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}
// Upper level call method
public class MyController : ApiController
{
  public string Get()
  {
    var jsonTask = GetJsonAsync(...);
    return jsonTask.Result.ToString();
  }
}

There are 2 programming approaches to the above deadlock.

  1. Stop mixing asynchronous and synchronous writing, and always write asynchronous code using async/await syntactic sugar
  2. Apply the ConfigureAwait(false) method to waiting asynchronous tasks

Most of the time SynchronizationContext works silently after asynchronous programming, but knowing this object is very useful to understand how Task, await/sync works.

This article will explain.

  1. async/await working mechanism
  2. The significance of SynchronizationContext in asynchronous programming syntactic sugar
  3. Why there is a deadlock

2. The await/async syntax sugar working mechanism

Microsoft has proposed the Task thread wrapper class and await/async to simplify the way of asynchronous programming.

② call the asynchronous method GetStringAsync , open the asynchronous task .

⑥ When the await keyword is encountered, the framework captures the synchronization context (SynchronizationContext) object of the calling thread and attaches it to the asynchronous task; at the same time, control is handed over to the upper-level calling function.

(7) Asynchronous task completion, through the IO completion port to notify the upper thread .

⑧ Execution of the successor code block through the captured thread synchronization context.

3. The meaning of SynchronizationContext

Let’s first look at the definition of SynchronizationContext in MSDN.

Provides the basic functionality for propagating synchronization contexts across various synchronization models. The purpose of such implemented synchronization models is to allow internal asynchronous/synchronous operations of public language runtime libraries to operate properly using different synchronization models.

☹️ This is not at all a human-readable explanation, and the explanation I give is that the context of the calling thread is saved during the thread switch and used to execute subsequent code after the asynchronous task completes using this thread-synchronous context.

What is the significance of this thread synchronization context?

As we all know: WinForm and WPF have a similar principle: long time consuming tasks are calculated in the background and the asynchronous results are returned to the UI thread.

At this point we need to capture the SynchronizationContext of the UI thread and pass this object into the background thread.

public static void DoWork()
{
    //On UI thread
    var sc = SynchronizationContext.Current;

    ThreadPool.QueueUserWorkItem(delegate
    {
       // do work on ThreadPool
        sc.Post(delegate
        {
             // do work on the original context (UI)
        }, null);
    });
}

SynchronizationContext identifies the thread environment in which the code is running, each thread has its own SynchronizationContext, and the synchronization context of the current thread can be obtained through SynchronizationContext.

In an asynchronous thread switching scenario, use SynchronizationContext to return to the calling thread.
NET frameworks have different SynchronizationContext subclasses (usually overriding the parent virtual methods) due to their unique needs: .

  • ASP.NET has AspNetSynchronizationContext
  • Windows Form has WindowsFormSynchronizationContext
  • WPF has DispatcherSynchronizationContext
  • ASP.NET Core, console programs do not have SynchronizationContext, SynchronizationContext.Current=null

AspNetSynchronizationContext maintains HttpContext.Current, user identity and culture, but in ASP. NET Core this information is naturally dependent on injection, so SynchronizationContext is no longer needed; another benefit is that no longer getting the synchronization context is also a performance improvement for Another benefit is that not getting the synchronization context is also a performance improvement.

Therefore, ConfigureAwait(false) is not required for ASP.NET Core applications, however, it is better to use ConfigureAwait(false) for the base library because you can’t guarantee that the upper layers will mix synchronous/asynchronous code.

4. Introduction code why deadlock occurs

Observing the quoted code, when control returns to the upper call function, the execution flow uses the Result/(Wait method) to wait for the result of the task. Result/Wait() causes the calling thread to block synchronously (waiting for the task to complete), and after the asynchronous task execution completes, it tries to execute the successor code using the captured synchronous context, thus forming a deadlock.

Because of this, we propose that

  • always use the await method in the calling function, so that the calling thread is waiting asynchronously for the task to complete and the successor code can be executed on the synchronous context of that thread
  • Apply the ConfigureAwait(false) method to an asynchronous task

ConfigureAwait(bool): true means that an attempt is made to execute the successor code in the captured SynchronizationContext of the original calling thread; false does not attempt to execute the successor code in the captured SynchronizationContext of the thread. ConfigureAwait(false) resolves deadlocks [caused by synchronous blocking of the calling thread], but synchronous blocking does not take advantage of asynchronous programming and is not highly recommended.

  • You will see that both of these deadlock mitigation solutions are actually specific to the SynchronizationContext.
  • ASP.NET Core and console programs, because of the captured SynchronizationContext=null, will choose a thread synchronization context to execute without deadlocking.

5. Summary

NET provides await/async syntax sugar to simplify asynchronous programming,

In asynchronous programming, the SynchronizationContext determines the environment in which subsequent code is executed. A deeper understanding of the background of this object and how it is implemented in different frameworks can help us avoid writing deadlock code.

Leave a Reply