SynchronizationContext & ConfigureAwait in C#

Alperen Bayramoğlu
3 min readAug 20, 2023

--

Photo by v2osk on Unsplash

SynchronizationContext is a representation of the current environment that our code is running. For example, in an asynchronous program, if we execute a unit of work in a different thread, we capture the current environment and put it into the SynchronizationContext, and hold it in the Task object.

Different frameworks have different communication protocols between threads. All threads have their own so if we delegate a job to another thread, the current SynchronizationContext's snapshot is taken and given to a new thread.

In the simplest form, SynchronizationContext shows where code may work. The base class SynchronizationContext exposes multiple virtual methods. One of them is the Posttakes delegateas a parameter. In the default implementation of the SynchronizationContext, the Postfunction sends the delegate parameter to the Thread Pool using QueueUserWorkItem.Frameworks inherit this base class and override the defaults:

  • In the Windows Forms application, there is a WindowsFormsSynchronizationContextclass and the Postmethod gives the delegate function parameter to the Control.BeginInvokefunction.
  • In the WPF application, there is a DispatcherSynchronizationContext implementation.
  • In the ASP.NET Core application, synchronization context implementation does not exist

Why does SynchronizationContext matter?

When we wait for a Task with await

  1. If the SynchronizationContext.Currentis not null, this value is the captured SynchronizationContext.
  2. If it is null, synchronization context is the current TaskScheduler( TaskScheduler.Defaultis the context of the thread pool)

Wrong use of SynchronizationContext with ConfigureAwaitmay result in deadlocks!

ConfigureAwait

ConfigureAwait can be used with any Taskor Task<T>object. ConfigureAwait specifies, how to behave when `await` is used inside the Taskor Task<T> object.

ConfigureAwait takes a single parameter named continueOnCapturedContextwith the default value true. If falseis given to this parameter, after awaiting is finished in the Task, Task may continue with different SynchronizationContext. For example when a user clicks the button in the WPF application… (in this situation the SynchronizationContext is a DispatcherSynchronizationContext)

public async void Button1_Click(){
var taskResult = await SomeTaskTodo().ConfigureAwait(false);
// ERROR: We cannot do this because context is not captured. We
// do not know what the textBox1 is due to the context is not
// captured and we may be in different thread in thread pool
textBox1.Text = taskResult;
}

Deadlocks

ConfigureAwait may be required for avoiding deadlocks. If the captured context is blocked by another operation that waits for a task to finish, a deadlock will occur. For example, if the UI thread synchronously waits for a task to finish deadlock will happen.

public static async Task<string> SomeWork(Uri uri)
{
using (var client = new HttpClient())
{
// await is used here without ConfigureAwait(false) context will be
// captured but it can’t because context has blocked synchronously
// Button1_Click function so DEADLOCK!
var s = await client.GetStringAsync(uri);
return s
}
}

public void Button1_Click(…)
{
// Waiting the result synchronously and the context is captured
// ConfigureAwait(false) is not used
var resultTask = SomeWork(…);
textBox1.Text = resultTask.Result;
}

For the solution, libraries should be independent of context and should use ConfigureAwait(false)as much as possible. As in the code above, we should not block asynchronous code synchronously.

Solution 1

var s = await client.GetStringAsync(uri).ConfigureAwait(false);

Solution 2

public void Button1_Click(…)
{
var result = await SomeWork(…);
textBox1.Text = result;
}

If we have work that depends on the context, we can’t use ConfigureAwait(false)

Some think to consider, ConfigureAwait is not block-scoped:

// Don’t capture and resume on any available thread
await DoSomethingAsync().ConfigureAwait(false);
// Capture and resume on whatever context was current at this point
await DoSomethingElseAsync();
// Don't capture and resume on any available thread
await DoAnotherThingAsync().ConfigureAwait(false);

--

--