Photo by v2osk Unsplash
Table of Contents
Open Table of Contents
SynchronizationContext
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 SynchronizationContext 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 Post takes delegate as a parameter. In the default implementation of the SynchronizationContext, the Post function 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 thePostmethod gives the delegate function parameter to theControl.BeginInvokefunction. - In the WPF application, there is a
DispatcherSynchronizationContextimplementation. - In the ASP.NET Core application, synchronization context implementation does not exist
Why does SynchronizationContext matter?
When we wait for a Task with await:
- If the
SynchronizationContext.Currentis not null, this value is the capturedSynchronizationContext. - If it is null, synchronization context is the current
TaskScheduler(TaskScheduler.Defaultis the context of the thread pool)
Wrong use of SynchronizationContext with ConfigureAwait may result in deadlocks!
ConfigureAwait
ConfigureAwait can be used with any Task or Task<T> object. ConfigureAwait specifies, how to behave when await is used inside the Task or Task<T> object.
ConfigureAwait takes a single parameter named continueOnCapturedContext with default value true. If false is 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 aDispatcherSynchronizationContext)
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);