In yesterday’s article, “Motivation for async/await in C#“, we have seen why asynchronous programming is important. We have also seen basic usage of the await
keyword, which requires its containing method to be marked as async
.
When learning to write asynchronous methods, it is not trivial to get the interactions between various methods (which may or may not be asynchronous) right. In fact, the examples in yesterday’s article which use an async void
method should normally only be used in event handlers, and even so, there are caveats to consider.
In this article, we’ll go through various different scenarios in which async
/await
can be used.
async Task methods
Let’s take another look at the asynchronous (event handler) method from yesterday’s article:
private async void Button_Click(object sender, RoutedEventArgs e) { var baseAddress = new Uri("http://mta.com.mt"); using (var httpClient = new HttpClient() { BaseAddress = baseAddress }) { var response = await httpClient.GetAsync("/"); var content = await response.Content.ReadAsStringAsync(); MessageBox.Show("Response arrived!", "Slow website"); } }
Try moving out the code into a separate method, and await
ing it from the event handler. You’ll find that you can’t await
an async void
method:
In order to be able to await
a method, it must return Task
(if it doesn’t need to return anything, such as void
methods) or Task<T>
(if it needs to return a value of type T
). We also append an –Async
suffix to the method name by convention to make it obvious for people who use such methods that they’re meant to be awaited.
Thus, this example becomes:
private async void Button_Click(object sender, RoutedEventArgs e) { await GetHtmlAsync(); } private async Task GetHtmlAsync() { var baseAddress = new Uri("http://mta.com.mt"); using (var httpClient = new HttpClient() { BaseAddress = baseAddress }) { var response = await httpClient.GetAsync("/"); var content = await response.Content.ReadAsStringAsync(); MessageBox.Show("Response arrived!", "Slow website"); } }
This is an example of an async Task
method, which does not return anything. Let’s change it such that it returns the HTML from the response:
private async void Button_Click(object sender, RoutedEventArgs e) { string html = await GetHtmlAsync(); } private async Task<string> GetHtmlAsync() { var baseAddress = new Uri("http://mta.com.mt"); using (var httpClient = new HttpClient() { BaseAddress = baseAddress }) { var response = await httpClient.GetAsync("/"); var content = await response.Content.ReadAsStringAsync(); return content; } }
We’ve changed the signature of GetHtmlAsync()
to return Task<string>
intead of just Task
. Correspondingly, we are now returning content
(a string
) from the method. At the event handler, we are now assigning the result of the await
into the html
variable. Apart from waiting asynchronously until the method completes, await
has the additional function of unwrapping the result from the Task
that contains it; thus html
is of type string
.
If you try removing the async
keyword from GetHtmlAsync()
, you’ll learn a little more about the actual function of the async
keyword:
Without async
, you are expected to return what the method advertises: a Task<string>
. On the other hand, if you mark the method as async
, the meaning of the method is changed such that you can return a string
directly. The underlying Task
-related plumbing is handled by the compiler.
Chaining Asynchronous Methods
In the previous section, we have seen how methods need to return a Task
in order to be await
ed. Typically, one async Task
method will call another and await
its result.
The chain of calls ends at a last async Task
, typically provided by the .NET Framework or other library, which interfaces directly with I/O (e.g. network or filesystem). It must be an asynchronous method; attempting to disguise a blocking call as async
here will lead to deadlocks.
async Task
may (and should) be used all the way from an incoming request to the final I/O library method in application types that support top-level asynchronous methods, such as Web API or WCF.
The situation is a little different for other applications (e.g. Windows Forms, WPF) that are event-driven. Asynchronous event handlers are a special case where we need to use async void
methods, as we have already seen in the WPF example from yesterday’s article:
async void methods
Event handlers are void
methods that are called by the runtime in a dispatcher loop. Thus, async void
methods are necessary to allow usage of await
within event handlers without requiring them to return a Task
However, as we have seen before, there is no way to await
an async void
method. This makes async void
methods very dangerous to use outside of their intended context, as I have detailed in “The Dangers of async void Event Handlers“. This is one of the more common mistakes when programming with async
/await
, and it is good to become familiar with the problems in order to avoid repeating the same mistakes in future.
Fake Asynchronous Methods
Sometimes, you’ll have an interface that requires asynchronous methods, yet your implementation does not need anything asynchronous in it. Let’s look at a practical example:
public interface ISimpleStorage { Task WriteAsync(string str); Task<string> ReadAsync(); }
You could implement this interface using a simple file as a backing store, in which case your methods will be suitably asynchronous:
public class FileStorage : ISimpleStorage { public async Task<string> ReadAsync() { using (var fs = File.OpenRead("file.txt")) using (var sr = new StreamReader(fs)) { var str = await sr.ReadToEndAsync(); return str; } } public async Task WriteAsync(string str) { using (var fs = File.OpenWrite("file.txt")) using (var sw = new StreamWriter(fs)) { await sw.WriteAsync(str); await sw.FlushAsync(); } } }
However, you could have another implementation which just uses memory as storage, and in this case there’s nothing asynchronous:
public class MemoryStorage : ISimpleStorage { private string str; public Task<string> ReadAsync() { return Task.FromResult(str); } public Task WriteAsync(string str) { this.str = str; return Task.CompletedTask; } }
In that case, your methods need not be marked async
. However, this means that you will actually need to return Task
instances from each method. If you have nothing to return, then just return a Task.CompletedTask
(available from .NET Framework 4.6 onwards). You can also use Task.FromResult()
to construct a task from a variable that you want to return.
Simplifying Single Line Asynchronous Methods
Consider the following asynchronous method:
static async Task WaitALittleAsync() { await Task.Delay(10000); }
Here, we are waiting for the delay to finish, and then returning the result.
Instead, we can just return the Task
itself, and let the caller do the await
ing:
static Task WaitALittleAsync() { return Task.Delay(10000); }
Once again, a method does not need to be marked async
if (a) it does not await
anything, and (b) it can return a Task
, rather than some other type that needs to be wrapped in a Task
.
Using async/await in Main()
Until recently, you couldn’t use async
/await
in a console application’s Main()
method. You can have an async Task Main()
method as from C# 7.1, but you need to make sure you’re using C# 7.1 first from your project properties -> Build -> Advanced…:
You can choose either “C# 7.1”, or “C# latest minor version (latest)”. With that, you can await
directly from within Main()
:
static async Task Main(string[] args) { await WaitALittleAsync(); }
Before C# 7.1, you had to use some other workaround to await
from Main()
. One option is to use a special AsyncContext such as the one written by Stephen Cleary. Another is to just move asynchrony out of Main()
and use a Console.ReadLine()
to keep the window open:
static void Main(string[] args) { Run(); Console.ReadLine(); } static async void Run() { await WaitALittleAsync(); }
Summary
- Use
async Task
methods wherever you can. - Use
async void
methods for event handlers, but beware the dangers even there. - Use the –
Async
suffix in asynchronous method names to make them easily recognisable asawait
able. - Use
Task.CompletedTask
andTask.FromResult()
to satisfy asynchronous contracts with synchronous code. - Asynchronous code in
Main()
has not been possible until recently. Use C# 7.1+ or a workaround to get this working.