C# 5.0 introduced two new keywords: async
and await
. These keywords have a very powerful effect that can be used without fully understanding them. This is a double-edged sword. It’s great to have a language feature that doesn’t take much time to implement, but at the same time, if there isn’t at least a basic understanding, then utilizing it correctly and troubleshooting any issues around it becomes very difficult.
It’s outside the scope of this blog post to do a deep dive into what exactly is happening when using these keywords, but hopefully you will have a better understanding, conceptually, so that you can leverage their functionality in an intelligent way.
async
The best way to describe the async
keyword is that it is an implementation detail. This means that although it is used when declaring a method, it is not part of the method signature itself. Any code that invokes a method marked as async
doesn’t know, nor does it care, that the method has been marked this way. async
is really a way of indicating to the C# compiler that this method needs to be compiled in a different manner than a typical method.
To drive the point home that async
is an implementation detail, think about it this way: async
is not valid to use on a method when you are defining an interface. If you write a class to implement the interface, async
is not included when you have Visual Studio stub out the interface implementation.
When should you mark a method as async
? The answer is very simple: whenever the keyword await
is used in the body of the method. If you mark a method as async
and at least one await
is not present, a compiler warning will be generated. The compiled code will still execute, but the method can be compiled in a more optimized way if the async
keyword is removed.
await
The next question is naturally “when do I use await
?” await
can be used with any awaitable type (duh!), but what is an awaitable type? Many developers would answer: System.Threading.Tasks.Task
or System.Threading.Tasks.Task<T>
. It is correct that those are two awaitable types, that is not the complete answer. The complete answer is a little more complicated. Whether or not a type is awaitable is determined by duck typing.
The duck typing rules (simplified) are that the type you want to await
must have a public method called GetAwaiter
that takes no parameters and returns an object that meets the following criteria:
- Implements either of the two interfaces:
INotifyCompletion
orICriticalNotifyCompletion
(both are located in theSystem.Runtime.CompilerServices
namespace) - Has a property called
IsCompleted
that returns a boolean - Has a method called
GetResult
. The method is not required to return anything (i.e. can be void)
All that being said, Task
and Task<T>
pass the duck typing test, and are likely going to be the only awaitable types you’ll encounter.
By convention, all methods that return an awaitable type should have the suffix of Async
at the end of the method name. This is not a requirement, but it does make it obvious to anyone who sees the method (either in code or via intellisense) that it returns an awaitable type.
Please note that at no point have I said “an awaitable method”. Methods are not awaitable, types are awaitable. The most common use of the await
keyword will look something like this:
var value = await SomeMethodAsync();
While it looks like the SomeMethodAsync
method is being awaited, it is not, the object it returns is being awaited.
When and How to use await
Just because a method returns an awaitable type doesn’t mean the await
keywords should be used. The await
keyword should only be used when the result of the awaitable type is needed or if the completion of the asynchronous operation represented by the awaitable type is required before continuing execution of the current method. Often the result is needed right away, but this isn’t always true. Take the following example:
var value1 = await GetValueAsync(); var value2 = await GetAnotherValueAsync();
A first glance, there may not be anything wrong with the above code, but there are potential performance problems. Let’s say GetValueAsync()
takes 4 seconds to complete and GetAnotherValueAsync()
takes 3 seconds to complete. Executing both lines of code will take approximately 7 seconds to complete. This is because the await
keyword causes the current method to suspend execution and the method will not resume executing until the asynchronous operation is complete. Assuming these two asynchronous methods are completely unrelated, there is no need to wait for the first one to complete before executing the second one, so the above code can be optimized by rewriting it this way:
var awaiter1 = GetValueAsync(); var awaiter2 = GetAnotherValueAsync(); await System.Threading.Tasks.Task.WhenAll(awaiter1, awaiter2); var value1 = awaiter1.Result; var value2 = awaiter2.Result;
There is a little more code, but in this case, the method will only take about 4 seconds to execute, because both asynchronous operations are happening in parallel. We could have awaited each awaiter individually, and nothing would have been wrong with that, but Task
has a static method that makes it easier to combine multiple awaiters into a single awaitable. To get the result of each asynchronous operation, invoke the Result
property on the awaiter. Please note: Invoking Result
on the awaiter before the asynchronous operation completes will cause the current thread to block until the operation has completed, so please only do this once you know the awaitable has completed. Using the await
keyword gives us this guarantee, so we know it is safe to call after it has been awaited.
If the method you are writing doesn’t have a need for the result of an asynchronous operation nor does it need a guarantee that the operation is complete before it finishes execution, then there may be no reason to await
the operation at all. One common reason to await
an operation even if the result is not needed: the method should handle exceptions that occur in the asynchronous operation. If this isn’t needed , then it’s entirely possible that the method may not need to await
anything, and if that’s the case, the method itself may not need to be marked as async
. Note: Even if a method is not marked as async
, it should still have the Async
suffix if it returns an awaitable type.
Hopefully this blog post has given you a better understanding of when and how to use async
and await
, and will save you time in the future when working with this language feature.