When working with C#’s async/await keywords, you need to know about the Synchronization Context. I did not understand it at first and it caused many headaches (or rather, migraines).
But before we talk about Synchronization Contexts, let’s lay some groundwork: DeadLocks and ConfigureAwait. Deadlocks
Sometimes you need to block and wait for the result of an asynchronous operation.
Ideally you should just use async code all-the-way, and not block, but there are times where it is necessary (I may explore the reasons in a future blog post).
If you choose block an async call, remember that blocking can be dangerous. Let’s examine a simple piece of Xamarin iOS code, notice the following:
- We have a button “click” event handler (a “touch” event in iOS).
- Inside the event handler we wait for the response from the GetDataAsync method by calling
.Result
- We do a simple
await
in the async method.
public async override void ViewDidLoad ()
{
base.ViewDidLoad ();
Button.TouchUpInside += (sender, e) => {
string data = GetDataAsync().Result; //Wait for result before continuing.
NavigationController.PushViewController( GetNextViewController(data) );
};
}
async Task<ViewModel> GetDataAsync()
{
///await and fetch the data from somewhere...
string result = await ServiceLayer.FetchDataAsync();
var viewModel = new ViewModel(result, DateTime.Now);
return viewModel;
}
What you may not expect is that when the user taps on the Button, the code will hang indefinitely. This happens because when GetDataAsync().Result
is called, the code sits and waits for a result, and it does so on the UI thread. While it does so, await ServiceLayer.FetchDataAsync()
is called.
It will execute, and then the code will try to resume on the same context (namely the UI thread), but the context is already blocked by the GetDataAsync().Result
call from before. Catch-22. Deadlock. Everybody is waiting on everyone else.
ConfigureAwait
There is a solution. Changing the GetDataAsync
method as follows solves the problem:
async Task<ViewModel> GetDataAsync()
}
String result = await ServiceLayer.FetchDataAsync()
.ConfigureAwait(false);
var viewModel = new ViewModel(result, DateTime.Now);
return viewModel;
}
ConfigureAwait(false)
tells our async code that when method execution continues after the await, that it does not have to do so on the same synchronization context as before, meaning it can execute on another thread, complete and return control to the UI thread. No more deadlock! If you are writing library code, it’s a good idea to use ConfigureAwait(false), because you don’t know if callers of your library may cause deadlocks.
The Synchronization Context
So what exactly is a ‘Synchronization Context’ and how does it relate to a Thread? You can have a look at if you are curious. Set a breakpoint in some of your own asynchronous code and add a watch for:
System.Threading.SynchronizationContext.Current
If the watch value is null
, it means there is no synchronization context on the current thread. However, sometimes it has a value because your code needs a synchronization context.
This is the case with most “UI” code like WPF and Xamarin iOS (and even ASP.NET). Most UI frameworks have a limitation in place where only one change to the UI is allowed at a time. A single UI thread is in charge of that. If 10 changes need to be made, then 10 changes need to be queued up and executed in sequence.
The limitation is in place to help the UI framework to avoid the nightmare of managing multiple threads updating the user interface at the same time, putting it in an inconsistent state. The synchronization context, if present, manages communication with the UI thread in an orderly manner.
(Although, a synchronization context can manage communication with other things too, for example the ASP.NET request context.) Remember that the CLR does not know about async/await. Await is simply a language feature found in C# and Visual Basic. When code gets compiled to MSIL, the async/await keywords are no longer there.
Scott Chamberlain has a great answer on StackOverflow that illustrates roughly what the compiled MSIL does and how execution proceeds based on whether or not there is a synchronization context.
Notice the if statement at the bottom that either posts to the synchronization context or runs the code normally in a Task:
public Task Example() {
Foo();
var syncContext = SyncronizationContext.Current;
return BarAsync().ContinueWith((continuation) =>
{
Action postback = () =>
{
string barResult = continuation.Result();
Baz(barResult)
}
if(syncContext != null)
syncContext.Post(postback, null);
else
Task.Run(postback);
});
}
An extension method
At NML we prefer to always state explicitly how we want the task continuation to occur. So even though a Task’s default is ConfigureAwait(true), we still specify it as such so that we are always cognizant of what’s happening “under the hood”.
However, when you look at a lot of code, some with ConfigureAwait(true) and some with ConfigureAwait(false), it’s not easy to spot where they differ. So we use either ConfigureAwait(false), or a useful extension method, ContinueOnCapturedContext(). It does the same thing, but just differentiates it from ConfigureAwait(false) in a more visual manner. The extension method is simple:
public static ConfiguredTaskAwaitable <TResult> ContinueOnCapturedContext <TResult> (this Task<TResult> t)
{
return t.ConfigureAwait (true);
}
To summarize
- When execution continues after an await, generated code ensures that the code runs on the same synchronization context that it did before (by default).
- The synchronization context’s job is simply to coordinate communication with a resource (UI thread, request context, etc).
- Use ConfigureAwait(false) to tell async code that it does not have to continue on the same context. It’s important to use this in library code to prevent deadlocks.