TPL and async/await Best Practices for the Busy Developer

This .Net async programming guide is a little bit different. It’s not a ‘how to’ or a deep dive. It’s more like a cheat-sheet for keeping out of trouble.

If you prefer, Jump to the TLDR;

The guide was born at Trade Me Ltd, New Zealand. Trade Me’s e-commerce platform is so popular here that it serves about half of our nation’s internal web traffic. Performance and stability are paramount, and I wrote these guidelines to help keep our asynchronous bits up-to-scratch.

TPL and async await for the busy developer - both ways

It’s increasingly common for teams to adopt the .Net Task Parallel Library (TPL) and the async and await keywords.

One big reason is the popularity of WebAPI, which is a fully-asynchronous library. UI programming, where work needs to be shifted off the UI thread, is another big area of uptake. Heaps of the libraries provided for Metro app development are also natively async.

Risks introduced by using the TPL and async/await include:

  • Deadlocks (the async equivalent of infinite loops).
  • Lost exception information (a debugging nightmare).
  • Process crashes (unobserved AggregateExceptions can crash your windows service or even IIS itself).
  • Surprisingly slow performance (e.g. you think things will be running concurrently, but they’re not).

Teams who maintain a partially-asynchronous codebase are most at-risk from the pitfalls of the TPL and async/await. Partially-async means there are conversions from async to synchronous. That includes all codebases which make even a single use of .Wait() or .Result! The risks are compounded by how quickly partially-async code can be hidden behind layers of abstraction. If people coding against interfaces don’t know there is async code ‘under the hood’, they won’t take precautions.

Guidelines

Following are 8 simple guidelines for successful use of the TPL and async/await. You could follow the TLDR prescriptively and be confident of producing stable and performant code, or check out the additional details if you are so-inclined. A basic knowledge of how to use the TPL and async/await is assumed. TPL’s parallelism APIs are out-of-scope for this guide, just as they are out-of-scope for most real-life situations.

TLDR; 8 Guidelines for optimal use of TPL and async/await

  1. Don’t do it. [more detail]
    If you can follow this guideline, you have all the rest covered.

  2. It is safest to use async code end-to-end. [more detail]
    Using .Wait(), .Result, or similar creates a point of conversion, and introduces the risk of encountering a deadlock or the unwieldy AggregateException. Prefer to use await and stay async.

  3. If in doubt, use .ConfigureAwait(continueOnCapturedContext: false) every time you use await. [more detail]
    This technique will defend against deadlocks, ensure maximum concurrency, and, in the rare case that it is inappropriate, it will fail early and fail loudly. Without this, async methods will block after using await until they can complete on the thread which started them, which is only necessary when you need the original SynchronisationContext (like if you explicitly access HttpContext.Current).

  4. If you must convert from async to synchronous, use a ‘Gate-keeper’ method, specifically for the purpose of conversion. [more detail]
    Hidden asynchronous code, error-handling mishaps and deadlocks can all be avoided by having a clearly sign-posted method which starts the async code on the ThreadPool and deals with AggregateExceptions (code sample).

  5. Capture unobserved and unhandled exceptions. [more detail]
    Missed AggregateExceptions can crash your server. Handling these events can prevent it and help you log missed exceptions: TaskScheduler.UnobservedTaskException and AppDomain.CurrentDomain.UnhandledException. There are some nuances for Windows Services and Windows Phone (see more detail).

  6. You can catch Exceptions as per normal when using await.
    Code called with await won’t cause an AggregateException. There is nothing else tricky to worry about, either. You can handle errors with an everyday trycatch.

  7. The TPL equivalent of returning void is returning async Task. [more detail]
    The return type of async void is intended for async events only. Anywhere else it will result in dodgy error handling behaviour, and leave the consumer with no way to control the call.

  8. Avoid fire-and-forget, or if you can’t, use ContinueWith for error handling. [more detail]
    Fire-and-forget is a problematic technique, and there is usually a better solution. If fire and forget must be used, error-handling best practice is to include a ContinueWith using TaskContinuationOptions.OnlyOnFaulted.

Clone the repo from github if you would like to step through code samples illustrating these points.

1. Don’t do it

Asynchronous programming comes with a bunch of extra complexity and risk. Unless you have a very clear need for concurrency, you may as well save yourself the hassle. Admittedly, if you need to use WebAPI or you are doing native GUI programming, you probably can’t avoid it.

2. It is safest to use async code end-to-end

Coding end-to-end asynchronous will save you all the headaches associated with partially-async code, and make error-handling much simpler. You won’t need to worry about AggregateExceptions, deadlocks or hidden async code.

Fully-async alternatives to conversion

.Wait() becomes await

public void DeleteSomething(int id)
{
    using (var client = new HttpClient())
    {
        client.DeleteAsync("http://example.com/things/" + id)
            .Wait();
    }
}

↳ becomes ↴

public async Task DeleteSomethingAsync(int id)
{
    using (var client = new HttpClient())
    {
        await client
            .DeleteAsync("http://example.com/things/" + id);
    }
}

.Result becomes await

public HttpResponseMessage GetSomething(int id)
{
    using (var client = new HttpClient())
    {
        return client
            .GetAsync("http://example.com/things/" + id).Result;
    }
}

↳ becomes ↴

public async Task<HttpResponseMessage> GetSomethingAsync(int id)
{
    using (var client = new HttpClient())
    {
        return await client
            .GetAsync("http://example.com/things/" + id);
    }
}

Task.WaitAll() becomes await Task.WhenAll(..)

public void DeleteAllTheThings(int[] ids)
{
    var tasks = ids.Select(DeleteSomethingAsync);
    Task.WaitAll(tasks);
}

↳ becomes ↴

public async Task DeleteAllTheThingsAsync(int[] ids)
{
    var tasks = ids.Select(DeleteSomethingAsync);
    await Task.WhenAll(tasks);
}

Of course, applying the async keyword to your method will mean the calling method needs to become async, and so on. If you keep applying async, right up your call-stack to the entry point, you will probably hit an event of some nature. If you make it an async event, there is no need for any conversion to a synchronous context, and that should be the goal.

Async equivalents for entrypoints

MVC Actions

public ActionResult Index()

↳ becomes ↴

public async Task<ActionResult> Index()

The MVC.Net framework will pick up this Action, by convention, using the standard routes. Just don’t append Aysnc onto the end of the method name (it is conventional to add Async when a method uses the async keyword, however in this case it would cut the wires.

WebForms PageLoad event

(in VB.Net, just for fun)

Sub PageLoad(sender As Object, e As EventArgs) _
    Handles Me.Load

↳ becomes ↴

Async Sub PageLoadAsync(sender As Object, e As EventArgs) _
    Handles Me.Load

Also requires the following in the page directive:

<%@ Page Async="true" ..

WPF windows forms button event

void OnButtonClick(object sender, RoutedEventArgs e)

↳ becomes ↴

async void OnButtonClickAsync(object sender, RoutedEventArgs e)

3. If in doubt, use .ConfigureAwait(continueOnCapturedContext: false) every time you use await

await DoSomethingAsync()
    .ConfigureAwait(continueOnCapturedContext: false);

The default TPL behaviour for continuing after an await implies .ConfigureAwait(continueOnCapturedContext: true). That means the method will not resume until it can do so with the original SynchronisationContext (effectively the thread on which it was started). This is good if you need to access a GUI Dispatcher or HttpContext.Current after the async call, but that’s not the case for most async calls.

There are three main advantages of using .ConfigureAwait(continueOnCapturedContext: false):

A. Defends your code from deadlocks
B. Optimal concurrent execution
C. Fail early and fail loudly

A. Defends your code from deadlocks

Even if another developer calls your async code and converts it to synchronous without following best practices, you will still be safe from deadlocks.

// *** Deadlock ***

public ActionResult Index()
{
    DoSomethingAsync().Wait();
    //never gets past this point

    ViewBag.Result = "Success";
    return View();
}

private async Task DoSomethingAsync()
{
    await Task.Delay(1000);
}

[runnable github sample]

The nastiest thing about this deadlock is that it will only occur in some runtime environments. For example, no deadlock would occur if you ran the above sample from an Nunit test. Nunit uses a different SynchronisationContext than .Net MVC, with different characteristics with regards to how it uses the TPL’s ThreadPool.

//*** No deadlock ***

public ActionResult Index()
{
    DoSomethingAsync().Wait();

    ViewBag.Result = "Success";
    return View();
}

private async Task DoSomethingAsync()
{
    await Task.Delay(1000)
    //*** difference ***
              .ConfigureAwait(continueOnCapturedContext: false);
}

[runnable github sample]

B. Optimal concurrent execution

It’s common for an async method to have some of its own work to do between various async calls. By default, it will do it’s own work on the thread which started it.

//Completes in 2 seconds

public async Task<ActionResult> Index()
{
    var doSomething = DoSomethingAsync();
    Thread.Sleep(1000);
    await doSomething;

    ViewBag.Result = "Success";
    return View();
}

private async Task DoSomethingAsync()
{
    //When the following command runs, the calling method (Index)
    //will resume
    await Task.Delay(1);
    //This method now waits to continue on the 'captured' thread
    //(the thread it started it on)
    Thread.Sleep(1000);
}

[runnable github sample]

If you tell an async method to release the context (the thread it started on), it will resume its own work on another thread. That way, it doesn’t matter whether the thread which started it is busy.

//Completes in 1 second

public async Task<ActionResult> Index()
{
    var doSomething = DoSomethingAsync();
    Thread.Sleep(1000);
    await doSomething;

    ViewBag.Result = "Success";
    return View();
}

private async Task DoSomethingAsync()
{
    //When the following command runs, the calling method (Index)
    //will resume
    await Task.Delay(1)
        // *** difference ***
              .ConfigureAwait(continueOnCapturedContext: false);
    //This method now happily resumes on any available thread
    //from the TPL's ThreadPool

    Thread.Sleep(1000);
}

[runnable github sample]

C. Fail early and fail loudly

In the occasional event that you use .ConfigureAwait(continueOnCapturedContext: false) when you actually needed to continue on the original context, you will see a null reference exception immediately. This is a bucketload easier to spot and debug than intermittent deadlocks.

//Object reference not set to an instance of an object

public async Task<ActionResult> Index()
{
    await DoSomethingAsync()
              .ConfigureAwait(continueOnCapturedContext: false);

    //System.NullReferenceException
    var output = "Success. Http verb used: " +
             System.Web.HttpContext.Current.Request.HttpMethod;

    ViewBag.Result = output;
    return View();
}

private async Task DoSomethingAsync()
{
    await Task.Delay(1000)
              .ConfigureAwait(continueOnCapturedContext: false);
}

[runnable github sample]

//No error

public async Task<ActionResult> Index()
{
    await DoSomethingAsync();
    // *** difference ***
    // We allow the default behaviour, which is to resume on the 
    // thread (captured context) we started with.

    var output = "Success. Http verb used: " +
             System.Web.HttpContext.Current.Request.HttpMethod;

    ViewBag.Result = output;
    return View();
}

private async Task DoSomethingAsync()
{
    await Task.Delay(1000)
        //note that it doesn't matter if async calls further down
        //the stack let go of the context. ConfigureAwait has no
        //upstream effect
              .ConfigureAwait(continueOnCapturedContext: false);
}

[runnable github sample]

4. If you must convert from async to synchronous, use a ‘Gate-keeper’ method, specifically for the purpose of conversion

If going async end-to-end is out-of-scope, it’s a good idea to be deliberate and explicit about the conversion from async to synchronous. Then you can make sure funny error handling scenarios are dealt with, and make other programmers aware of the async stuff behind your synchronous interfaces. It is also an opportunity to protect against deadlocks in case the underlying async code doesn’t always follow best practices.

The idea of the Gate-keeper method is to have a synchronous version of your method sitting side-by-side with the Async version. The synchronous version is the Gate-keeper whose sole purpose is to convert the async version to synchronous.

Recommended features of a conversion Gate-keeper Method:

  • Try to place it on an intuitive ‘fence-line’ for your asynchronous code.
    E.g. If you have a service layer which does asynchronous stuff, put the gate-keeper method in the public interface.
  • Clearly sign-post it with comments pointing out the asynchronous equivalent which the gate-keeper method wraps.
    This is best done with the ObsoleteAttribute, so people calling the method can’t miss the comments.
  • Use Task.Run(() => FooAsync()).Result (or .Wait) at the point of conversion.
    This starts the asynchronous code directly on the TPL’s ThreadPool, which will prevent attempts to capture the original SynchronisationContext and safeguard against deadlocks.
  • Handle and log AggregateException.
    When converting to synchronous (.Wait, .WaitAll, .Result), we will get AggregateException. It wraps further exceptions which hold the real error information, so all the InnerExceptions need to be logged.
  • If the inner exceptions in the AggregateException include a type which is intended to be handled specifically by the caller, re-throw it.
    Handling of specific types of exceptions should be left for the caller or the real (async) method.
  • Finally, if no predicted Exception type is found, wrap the AggregateException in a regular exception and throw that.

Illustration of Task.Run protecting from deadlocks

//Deadlock

public ActionResult Index()
{
    DoSomethingAsync().Wait();
    //never gets past this point

    ViewBag.Result = "Success";
    return View();
}

private async Task DoSomethingAsync()
{
    await Task.Delay(1000);
}

[runnable github sample]

//No deadlock

public ActionResult Index()
{
    //this protects from deadlocks by starting the async method
    //on the ThreadPool
    Task.Run(() => DoSomethingAsync()).Wait();

    ViewBag.Result = "Success";
    return View();
}

private async Task DoSomethingAsync()
{
    await Task.Delay(1000);
}

[runnable github sample]

Gate-keeper method example

Method you need to convert

/// <summary>
/// Divides one number by another.
/// </summary>
/// <exception cref="DivideByZeroException"></exception>
/// <returns></returns>
public async Task<int> DivideSlowlyAsync(int dividend, int divisor)
{
    await Task.Delay(2000)
        .ConfigureAwait(continueOnCapturedContext: false);
    return dividend/divisor;
}

Conversion Gate-keeper Method

/// <summary>
/// Synchronous equivalent of DivideSlowlyAsync.
/// Divides one number by another. 
/// </summary>
/// <exception cref="DivideByZeroException"></exception>
/// <returns></returns>
[Obsolete("Wherever possible, use DivideSlowlyAsync and " +
          "refactor for end-to-end asynchronicity. Otherwise, " +
          "if you must have partially asnyc code, use this " +
          "Gate-keeper Method convert.")]
public int DivideSlowly(int dividend, int divisor)
{
    try
    {
        return
            Task.Run(() => DivideSlowlyAsync(dividend, divisor))
                .Result;
    }
    catch (AggregateException ae)
    {
        //Flatten the InnerExceptions, so you don't need to use 
        //recursion.
        var flatAe = ae.Flatten();

        //Rethrow any predicted exception types we declared
        foreach (var e in flatAe.InnerExceptions)
        {
            if (e is DivideByZeroException) throw e;
        }

        foreach (var e in flatAe.InnerExceptions)
        {
            //todo:log each of the InnerExceptions
        }

        throw new Exception("Stopped an AggregateException", ae);
    }
}

[runnable github sample]

5. Capture unobserved and unhandled exceptions

If left unchecked, stray AggregateExceptions can crash an entire process, like your windows service, or even IIS itself. If you follow the other guidelines in this post, you will avoid this problem, but as a safety net it is best to handle the following events.

From .Net 4.5, it is no longer the default behaviour for unobserved exceptions to kill the process, however they still contain error information which will be lost unless you use these events to log them.

This is not a substitute for following best practices with error handling, as these events are not guaranteed to fire in absolutely all circumstances, and even when they do it’s too late to gracefully handle the errors.

TaskScheduler.UnobservedTaskException

This event will catch unobserved Exceptions, such as when a Task is started, but never awaited, and then it throws an error.

Put the following in application startup:

TaskScheduler.UnobservedTaskException +=
    (sender, exceptionEventArgs) =>
    {
        exceptionEventArgs.SetObserved();
        var aggregateException = exceptionEventArgs.Exception;
        var flatAe = aggregateException.Flatten();
        //todo: log the flatAe.InnerExceptions
        //todo: rethrow if you want to allow the process to crash.
    };

This event only fires if the error’d Task makes it into the finalized state. There are some edge cases where this might not happen (such as if garbage collection never gets a chance to process). As such, it’s important to also handle AppDomain.CurrentDomain.UnhandledException.

AppDomain.CurrentDomain.UnhandledException

This event is a last-ditch effort to catch slippery exceptions. If this event has fired, your application is already in the process of crashing. There is no recovering from this point, but you can still at least log the exception.

Put the following in application startup:

AppDomain.CurrentDomain.UnhandledException +=
    (sender, exceptionEventArgs) =>
    {
        var aggregateException =
            exceptionEventArgs.ExceptionObject
                as AggregateException;
        if (aggregateException != null)
        {
            var flatAe = aggregateException.Flatten();
            //todo: log flatAe.InnerExceptions
        }
        else
        {
            var exception = (Exception)
                exceptionEventArgs.ExceptionObject;
            //todo: log exception
        }
        //application will now die
    };

Windows Services and UnobservedTaskException / UnhandledException

When a Windows Service entrypoint starts the service class (ServiceBase.Run), the service gets new application context from the Service Control Manager. The UnobservedTaskException and UnhandledException events will never fire if you assign the handlers in Program.cs or in the service class’s constructor. You must assign the handlers in the OnStart(string[] args) method of your service.

Windows Phone and UnobservedTaskException

If you want to prevent your Metro / WinRT app from crashing, even when unobserved exceptions make their way up to the UI thread, you need to register a custom wrapper for the SynchronizationContext.

This post does a great job of laying the solution out. It is an adaptation of the work done by Mark Young (@kiwidev). There is a later derivative here which seeks to provide multi-platform support.

7. The async equivalent of returning void is returning async Task

Returning async void is intended for async event handlers only. If you have no return type on an async method, just return Task. Without a Task, the caller has no control over the async call. Async void methods also have different error-handling semantics, so exceptions thrown from async void methods can’t be caught naturally.

When you are writing an async event, you could make it easier to unit test by putting nothing in the async void method except a call to another async method which returns Task. Unit testing frameworks generally handle the await keyword just fine when it is used to call a method which returns Task.

public async void MyButtonClickAsync()
{
    await MyButtonActionAsync()
            .ConfigureAwait(continueOnCapturedContext: false);
}

public async Task MyButtonActionAsync()
{
    //todo: logic ..
}
[Test]
public async void MyButtonActionAsyncDoesNotCrash()
{
    //arrange
    var buttonActionTask = new TestableAsyncEvents()
        .MyButtonActionAsync();

    //act
    await buttonActionTask;

    //assert
    Assert.That(buttonActionTask.Status,
                Is.EqualTo(TaskStatus.RanToCompletion));
}

8. Avoid fire-and-forget, or if you can’t, use ContinueWith for error handling.

There is almost always a better solution to any problem than fire-and-forget in native GUIs or windows services. In the context of a web request, there is no guarantee that the worker process will even continue to exist after the HTTP response has been sent.

public ActionResult Index()
{
    //todo: Stop it!
    Task.Run(() => GoDoSomething());

    return View();
}

Consider using a queuing agent like Hangfire, or a job scheduling system like Quartz.Net.

That said, if you must use fire-and-forget, then use ContinueWith for error handling with TaskContinuationOptions.OnlyOnFaulted:

Task.Run(() => GoDoSomething())
    .ContinueWith(task =>
    {
        var aggregateException = task.Exception;
        var flatAe = aggregateException.Flatten();
        //todo: log the flatAe.InnerExceptions

    }, TaskContinuationOptions.OnlyOnFaulted);

Source material

This guide should be sufficient for keeping out of trouble, but for completeness, here are some of the articles which went into its inception.

Best Practices in Asynchronous Programming – MSDN
Should I expose synchronous wrappers for asynchronous methods? – MSDN
The Beginner’s Guide to Exception Handling with the TPL – Amir Zuker
Don’t Block on Async Code – Stephen Cleary
Task Exception Handling in .NET 4.5 – MSDN

2 thoughts on “TPL and async/await Best Practices for the Busy Developer

Leave a comment