C#'s async, await, and .Result


In Market Invoice, there are many places where async and await are used. Recently, I introduced a bug that an operation gets deadlocked by replacing await with .Result. I was bitten hard :-)

Non-blocking execution

When using async and await, C# run time generates a state machine in the background

[sourcecode language="csharp"]
public async Task CallingMethodAsync()
{
Task<int> longRunningTask = LongRunningOperationAsync(); // 1)
// independent work which doesn't need the result of LongRunningOperationAsync can be done here

//and now we call await on the task
int result = await longRunningTask; // 2)

//use the result
Console.WriteLine(result);
}

public async Task<int> LongRunningOperationAsync()
{
await Task.Delay(1000); //1 seconds delay
return 1;
}
[/sourcecode]

1) LongRunningOperationAsync is running. But it doesn't block the execution of CallingMethodAsync, until the execution point reaches 2)

Now the execution point reached 2). If LongRunningOperationAsync() is fully done, the result will be ready, and it will be assigned to result straight away. However, if LongRunningOperationAsync() is still running, the execution of CallingMethodAsync will stop there, waiting until LongRunningOperationAsync() finishes. Once it finishes, CallingMethodAsync will resume the execution.

Call-back without its hell

Let's look at Eric's Serve Breakfast example.

[sourcecode language="csharp"]
void ServeBreakfast(Customer diner)
{
var order = ObtainOrder(diner);
var ingredients = ObtainIngredients(order);
var recipe = ObtainRecipe(order);
var meal = recipe.Prepare(ingredients);
diner.Give(meal);
}
[/sourcecode]

In this example, every customer must wait until the previous customer's breakfast is fully prepared and served. You can see people would get angry very soon.

In order to receive orders while preparing for breakfast, you have to take orders in an asynchronous manner. It will bring it javascript' call-back hell.

[sourcecode language="csharp"]
void ServeBreakfast(Diner diner)
{
ObtainOrderAsync(diner, order =>
{
ObtainIngredientsAsync(order, ingredients =>
{
ObtainRecipeAsync(order, recipe =>
{
recipe.PrepareAsync(ingredients, meal =>
{
diner.Give(meal);
})})})});
}
[/sourcecode]

The code is not very readable. Computers may like it, but humans are not good at following up the callbacks.

This can be rewritten in the new style, reads much more nicely.

[sourcecode language="csharp"]
async void ServeBreakfast(Diner diner)
{
var order = await ObtainOrderAsync(diner);
var ingredients = await ObtainIngredientsAsync(order);
var recipe = await ObtainRecipeAsync(order);
var meal = await recipe.PrepareAsync(ingredients);
diner.Give(meal);
}
[/sourcecode]

Now, the methods, ObtainOrderAsync() doesn't return order. It returns Task<Order>. It's a callback pointer. When the execution finishes, it return the result, and order is passed into ObtainIngredientsAsync()

await or .Result

Stephen Cleary recommends using await over Result.

First, await doesn't wrap the exception in an AggregateException, which represents one or more errors that occur during application execution. So, you will see the real exception, not the bland AggregateException. .Result wrap an exceptions that happens in the async method into AggregateException.

[sourcecode language="csharp"]
try {
details = await _service.GetDetails(personId);
...
} catch (ApplicationException) { // this catch will work, as await pass the exception as it is.
...
}
[/sourcecode]

For .Result, you have to catch AggregateException.

Second, Result / Wait can cause deadlocks. The async method will continue to run, and the task will be returned to it. When the task comes back, and if it's not completed yet, it will hang in the current context.

[sourcecode language="csharp"]
public class CompanyDetailsController : ApiController
{
public string Get()
{
var task = GetCompanyDetails(...);
return task.Result.ToString(); // if task hasn't been completed, this will block the thread.
}
}

public static async Task<CompanyDetails> GetCompanyDetails(Uri uri)
{
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri);
return CompanyDetails.Parse(jsonString);
}
}
[/sourcecode]

Preventing the deadlock

ConfigureAwait

[sourcecode language="csharp"]
await Task.Delay(1000).ConfigureAwait(
continueOnCapturedContext: false);
// Code here runs without the original context. (if the original context is UI thread, then UI thread context)
[/sourcecode]

By using ConfigureAwait, you enable parallelism that the asynchrounous code can run in parallel with the thread the original context is in. As a result, you can avoid the deadlock

avoid Result / Wait

As Result causes deadlocks, don't use it. Instead, favour await and use async on the method all they down or up.

 

Resources