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.
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
- Don’t do it. [more detail]
If you can follow this guideline, you have all the rest covered. -
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 unwieldyAggregateException
. Prefer to useawait
and stay async. -
If in doubt, use
.ConfigureAwait(continueOnCapturedContext: false)
every time you useawait
. [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 usingawait
until they can complete on the thread which started them, which is only necessary when you need the originalSynchronisationContext
(like if you explicitly accessHttpContext.Current
). -
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 withAggregateException
s (code sample). -
Capture unobserved and unhandled exceptions. [more detail]
MissedAggregateException
s can crash your server. Handling these events can prevent it and help you log missed exceptions:TaskScheduler.UnobservedTaskException
andAppDomain.CurrentDomain.UnhandledException
. There are some nuances for Windows Services and Windows Phone (see more detail). -
You can catch Exceptions as per normal when using
await
.
Code called withawait
won’t cause anAggregateException
. There is nothing else tricky to worry about, either. You can handle errors with an everydaytry
–catch
. -
The TPL equivalent of returning
void
is returningasync Task
. [more detail]
The return type ofasync 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. -
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 aContinueWith
usingTaskContinuationOptions.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); }
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); }
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); }
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); }
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); }
//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); }
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 theObsoleteAttribute
, 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 originalSynchronisationContext
and safeguard against deadlocks. - Handle and log
AggregateException
.
When converting to synchronous (.Wait, .WaitAll, .Result
), we will getAggregateException
. It wraps further exceptions which hold the real error information, so all theInnerExceptions
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); }
//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); }
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); } }
5. Capture unobserved and unhandled exceptions
If left unchecked, stray AggregateException
s 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
cool stuff, thanks for sharing this !
LikeLike
This is almost one of the best post i read ever on the async await subject. I have encountered most of the problems your article is pointing. It is sure your advices will be benefit for my next challenges… Thank you
LikeLike