Asynchronous Programming Best Practices in C#

C# classes contain a mixture of three asynchronous patterns:

  • Asynchronous Programming Model (APM) uses the IAsyncResult interface and requires async methods to be defined as BeginProcess and EndProcess methods (e.g., BeginSend/EndSend methods for asynchronous Socket send operations).
  • Event-based Asynchronous Pattern (EAP) was introduced with .NET Framework 2.0 and requires that asynchronous method names end with "Async" and uses event types, delegates, and custom EventArgs classes.
  • Task-based Asynchronous pattern (TAP) was introduced in .NET Framework 4.0 and Microsoft recommends that you use TAP for new projects. TAP uses Task-objects and only requires a single asynchronous method which can be "awaited", in contrast to APM and EAP which require more than one method to achieve asynchrony. Like EAP, you should end your TAP method names with "Async".

However, if you must use APM instead of TAP, do not mix async/await code with code which calls Task.Result and Task.Wait(), your code must be .Result/.Wait() all the way down (or async/await all the way down). Combining the two makes it extremely likely that you will encounter a deadlock at some point.

The sole exception to this rule used to be the static main entry method for a console app, which would not compile if defined as static async Task<int> Main(string[] args). If your main program relied on an async method call, you had to workaround this issue with the solution below:

csharp
static int Main(string[] args)
{
  DoAsyncWork().GetAwaiter().GetResult();

  Console.WriteLine("\nPress ENTER to continue...");
  Console.Read();
  return 0;
}

static async Task DoAsyncWork()
{
  await ExpensiveComputationAsync();
}

However, with the release of C# 7.1 your Main method can now be async:

csharp
static async Task<int> Main(string[] args)
{
  await ExpensiveComputationAsync();

  Console.WriteLine("\nPress ENTER to continue...");
  Console.Read();
  return 0;
}

If your main method does not return a value for the exit code, you can also define a main method that returns a Task object:

csharp
static async Task Main(string[] args)
{
  await AnotherAsyncMethod();
}

I will give an example of a console app that uses this new language feature in a future post. Since TAP is the recommended pattern, let's focus on best practices for async code that relies on the Task Parallelism Library (TPL).

TPL Best Practices

Avoid using Task.Factory.StartNew in almost every scenario in favor of Task.Run

The reasons for this are explained in this blog post by Stephen Cleary. The major reason to avoid StartNew is because it does not understand async delegates. Rather than the Task returned by StartNew representing the async delegate, it represents only the beginning of the delegate. Please read his post for more detail and examples of the pitfalls introduced by indiscriminate use of StartNew.

Cleary also argues that the options available with StartNew such as LongRunning and PreferFairness should only be used after an application has been profiled to ensure these options are actually going to have a significant impact. Typically, using Task.Run will provide nearly the same efficiency.

Avoid async void in every scenario besides event handlers

In an article from MSDN Magazine, Cleary gives three guidelines for using async/await. async methods can only have three return types: Task, Task<T> and void. The only reason void is allowed is to enable asynchronous event handlers. async event handlers are necessary but can be dangerous because exceptions thrown from async void methods can't be caught in the normal way with a try/catch block. In C#, these exceptions can be caught by using the catch-all AppDomain.UnhandledException

Another reason to avoid async void is that, unlike Task and Task<T> objects, methods returning void cannot be awaited or used with methods like Task.WhenAny and Task.WhenAll. This makes it difficult to determine the status of the method and whether it has completed. Because of these issues with exception handling and status monitoring, async void methods are difficult to unit test. Please see the article for detailed examples.

await the result of ConfigureAwait(false) whenever you can

Doing so is especially important when you are creating a library that will be used by client code. Consider the method below which retrieves the text of a webpage:

csharp
public async Task<string> GetUrlContentAsString()
{
    using (var httpClient = new HttpClient())
    using (var httpResonse = await httpClient.GetAsync("https://aaronluna.dev"))
    {
        return await httpResonse.Content.ReadAsStringAsync();
    }
}

If every client would call our method like this: await GetUrlContentAsString();, everything would be perfect. Of course, this will not always be the case. Now, consider what would happen if a client were to do the following:

csharp
public void GetUrlContentButton_Clicked(object sender, RoutedEventArgs e)
{
    var urlContents = GetUrlContentAsString().Result;
}

GUI applications have a SynchronizationContext that permits only one chunk of code to run at a time. When the await completes, it attempts to execute the remainder of the async method within the captured context. But that context already has a thread in it, which is (synchronously) waiting for the async method to complete. They’re each waiting for the other, causing a deadlock.

This can be fixed by adding ConfigureAwait(false) to our original method wherever await is used:

csharp
public async Task<string> GetUrlContentAsString()
{
    using (var httpClient = new HttpClient())
    using (var httpResonse = await httpClient.GetAsync("https://aaronluna.dev").ConfigureAwait(false))
    {
        return await httpResonse.Content.ReadAsStringAsync().ConfigureAwait(false);
    }
}

To summarize this third guideline, you should use Configure­Await(false) when possible. Context-free code has better performance for GUI applications and is a useful technique for avoiding deadlocks when working with a partially async codebase. If you're creating a library that’s potentially shared with desktop applications, consider using ConfigureAwait(false) in the library code.