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
WindowsFormsSynchronizationContext
class and thePost
method gives the delegate function parameter to theControl.BeginInvoke
function. - 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
:
- If the
SynchronizationContext.Current
is not null, this value is the capturedSynchronizationContext
. - If it is null, synchronization context is the current
TaskScheduler
(TaskScheduler.Default
is 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);