Quick Reminder: async Pitfalls in .NET

This blog post might seem a bit desperate: the async hype should be over, right? After all, the feature was introduced some time back in 2011 and, just to be clear: all I'm going to say has been blogged about in more detail at least three years ago. Maybe I have never read it. Maybe I forgot. Links follow.

In any case, there are more and more libraries that offer (or require) async, so the motivation is no longer curiosity or dire performance need - async, or at least understanding it, is becoming a neccessity. Don't get me wrong, it's a good thing that more and more libraries allow (easier) asynchronous programming, of course, but where there are libraries, there are wrappers and unfortunately, that's where it bites you.

Be careful when writing async library wrappers.

Let's say you use some fancy, shiny new async-enabled library (such as the MongoDB C# Driver 2.0). Let's also say you're a bit lazy: You don't want to start a new project with the old, obsolete version, but you don't want to go async in the entire stack just yet. Instead, you come up with a small (async) wrapper: a nice place to isolate some conventions maybe, or a wrapper to decouple dependencies:

public class MyBadWrapper
{
  public async Task<string> WrapperAsync()
  {
    return await SomeLibrary.GetStringAsync();
  }
}

Perhaps you're one of the careful ones who write a test first, or come up with an isolated small console application to test your stuff. Tada! It works, nice, right? No!

No? Unfortunately, above wrapper is evil when used in a web or UI context, but, to add to the confusion, it does work on the console. Consider the following nancy handler for an HTTP GET to /deadlock:

Get["/deadlock"] = _ =>
{
  // We're lazy and use .Result to get the result synchronously:
  var result = MyBadWrapper.Wrapper().Result;
  return Response.AsJson(new { message = result });
};

What will happen? Well, I guess the endpoint's name offers a hint, but this call will, in fact, deadlock even though the test worked.

The cause for the deadlock, in short, is that the .Result call synchronously waits for the Task to finish, but when the Task has finished, it will wait for the SynchronizationContext of the original method to become available again. But that context is responsible for waiting for the Task to finish. Deadlock. Like I said in the introduction, Stephen Cleary explained this in more detail back in 2012, and I recommend reading that article.

Understanding the Cure

The common cure is to use ConfigureAwait(false), which essentially tells the Task not to resume on the original SynchronizationContext, thus avoiding the deadlock. What's important to realize, however, is that this isn't passed up the call stack within the Task. Let's say SomeLibrary is implemented like this:

public class SomeLibrary 
{
    public static async Task<string> GetStringAsync()
    {
        // yup, the library is ok and calls ConfigureAwait(false):
        await Task.Delay(100).ConfigureAwait(false);
        return "string";
    }
}

Then one might assume that your minimalistic one-line wrapper simply passes that Task on, together with its configuration not to return to the synchronization context. But it doesn't. Instead, you have to call configureAwait everywhere in the entire call stack, or at least in all those methods that are exposed publically and could be called by someone synchronously.

Try not to think of Task<> as objects that can be passed upstream; think of them as boxes that wrap other Task<> objects, like Task<Task<Task<Task<T>>>>

So let's fix the wrapper:

public class MyGoodWrapper
{
  public async Task<string> WrapperAsync()
  {
    return await SomeLibrary.GetStringAsync().ConfigureAwait(false);
  }
}

If you think about it, that makes complete sense. After all, when I really do need to resume in the same context, I must not have to rely on the implementation details of some library method downstream. Also in 2012, Stephen Toub wrote two articles on async wrappers to sync methods and vice versa.

Note that, if some library is broken in that it doesn't set ConfigureAwait(false), you can't fix that upstream. The following code is complicated and won't fix the problem:

Get["/deadlock"] = _ =>
{
  var task = MyBadWrapper.Wrapper().ConfigureAwait(false);
  var result = task.GetAwaiter().GetResult();
  return Response.AsJson(new { message = result });
};

Best Practices

  1. Use async everywhere if you can
  2. Use ConfigureAwait(false) everywhere possible
  3. Read the linked articles that explain the gritty details
  4. Don't use void async

What the Whole Thing Should Look Like

To wrap this up, here's the all-async-non-deadlocking version that should also offer best performance (hey, I didn't say it did anything remotely useful):

public static class MyWrapper
{
    public static async Task<string> WrapperAsync()
    {
        return await SomeLibrary.GetStringAsync().ConfigureAwait(false);
    }
}

public static class SomeLibrary
{
    public static async Task<string> GetStringAsync()
    {
        await Task.Delay(100).ConfigureAwait(false);
        return "string";
    }
}

public class HelloWorldModule : NancyModule
{
    public HelloWorldModule()
    {
        Get["/", true] = async (ctx, ct) =>
        {
            var result = await MyWrapper.WrapperAsync().ConfigureAwait(false);
            return Response.AsJson(new { message = result });
        };
    }
}