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.,
<code>BeginSend</code>
/<code>EndSend</code>
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:
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:
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:
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:
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:
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:
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 ConfigureAwait(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.