03 May 2015 Comment

There’s a little motif that I’ve seen dotted around async C# code. It seems to surface when developers find themselves needing to consume an asynchronous API without yet having fully grasped how async works.

The specifics differ, but the gist is always the same. There’s a service layer, which makes an async call to a third party with ConfigureAwait(false):

// Service layer
public async Task<Thing> RetrieveThingAsync(int id)
{
    var result = await _thirdParty.ThingById(id)
                                  .ConfigureAwait(false);
    return MapResult(result);
}

… and there’s a presentation layer - in this example, ASP.NET MVC - that calls .Result on the service method and updates the UI:

// Presentation layer
public ActionResult ShowThing(int id)
{
    var thing = _thingService.RetrieveThingAsync(id).Result;
    return View(thing);
}

If your code follows this pattern, you probably got there by calling .Result on an async method, finding yourself faced with a deadlock, searching for a solution, skimming a blog post, adding ConfigureAwait(false) in your service, finding the deadlock resolved and moving on. Problem solved! End of blog post.

But there’s a catch.

This code seems innocuous enough and - truth be told - it is (in the context of ASP.NET, at least). This code doesn’t hide any nasty dormant bugs. It won’t cause memory leaks, buffer overflows or race conditions. It won’t break your heart, steal your wallet or burn your dinner. The deadlock really is fixed and the code will work.

What it will do, though, is the exact opposite of what you (probably) intended when you wrote it.

The problem with this code is that it is not asynchronous.

Not asynchronous?

When you call .Result on an incomplete Task, the thread executing the method has to sit and wait for the task to complete, which blocks the thread from doing any other useful work in the meantime. This negates the benefit of the asynchronous nature of the task.

In our case, RetrieveThingAsync makes a non-blocking request to an asynchronous third-party API, meaning the thread calling the method needn’t sit and wait for it to complete - it can go away and complete other tasks while the request (which might be an HTTP request, file IO or other relatively long-running task that does not require an active thread) is processed.

This is achieved with the await keyword, which effectively says ‘return an incomplete task (representing the third-party API call) to the caller and wire up the rest of this method as a callback when that task completes.’

In the case of ASP.NET MVC, the calling thread in question is an ASP.NET worker thread. This means that while one (web) request is waiting for the result of the asynchronous API call, the thread initiating the call can go and serve another.

However, because ShowThing calls RetrieveThingAsync.Result, the thread serving the request must sit and wait for the task to complete. It can’t be used to process another request from another user. It’s blocked.

In a single-threaded Windows Forms application, this mistake might make itself evident pretty quickly: long-running calls that should be asynchronous will start affecting the application’s responsiveness as the UI thread sits and waits for them to complete. In a web application, it’s not quite as obvious. When a browser makes a web request, it has to sit and wait for the response either way.

This lack of symptoms is why the mistake is easy to make and difficult to spot. Whether or not your server-side code is asynchronous makes essentially no difference to an individual user.

The difference to you as a developer, though, is that you’ve lost the performance and scalability benefits of freeing up your worker threads when they’re waiting for the result of non-blocking operations. The benefit of server-side asynchronous web programming isn’t one of UI responsiveness, it’s one of scale (and, in the case of multiple simultaneous tasks, concurrency).

The solution

Once you know there’s a problem, the solution is easy. In fact, it was under your nose all along. That very same blog post that gave you the ConfigureAwait workaround also gave you the solution to this problem (which, incidentally, would also have fixed the original deadlock). Make sure that any code that calls an async method is async itself.

// Presentation layer
public async Task<ActionResult> ShowThing(int id)
{
    var thing = await _thingService.RetrieveThingAsync(id);
    return View(thing);
}

By making ShowThing async and awaiting RetrieveThingAsync instead of calling .Result, our controller action now wires up our response (in this case a ViewResult) as a continuation of the Task and returns the incomplete Task up the call stack to the framework, which, in turn, frees up the worker thread to serve another request while the task completes.

Conclusion

The async and await keywords are powerful tools but their effects are not always self-evident and neither are their pitfalls. This post has demonstrated an example of when code that might appear intuitively asynchronous actually isn’t and, hopefully, provided some useful insight as to why that’s the case and what you can do about it.