What’s the deal with async
and await
in C#? Why should a .NET developer in 2019 need to know what it is and how to use it?
Perhaps you’ve used async/await
but find yourself having to go back and figure it out again?
I’ve had to figure out the hard way that, for example, using ConfigureAwait(false).GetResult()
doesn’t magically make your async method “work” synchronously.
But this isn’t an in-depth look at the internals of async/await
and how to get fancy with async/await
.
This is “Async/await for the rest of us”. Who are the rest of us?
We:
- Might have never used
async/await
- Are not Microsoft gurus
- Are not C# gurus
- Admittedly forget how to use
async/await
properly at times. - Might have to go back to Google and figure out why a method can return a
Task
but not use theasync
keyword. - Might wonder why a method that’s not marked as
async
can be awaited by it’s caller
This - I hope - is an article for “the rest of us” that’s to the point and practical.
P.S. If you do want to dig into this topic more the best starting point is I’d suggest starting with this article.
Why Async?
What’s the benefit of using async/await
?
For Web Developers
If you build web apps using .NET technologies - the answer is simple: Scalability.
When you make I/O calls - database queries, file reading, reading from HTTP requests, etc. - the thread that is handling the current HTTP request is just waiting.
That’s it. It’s just waiting for a result to come back from the operating system.
Performing a database query, for example, ultimately asks the operating system to connect to the database, send a message and get a message in return. But that is the OS making these requests - not your app.
Using
async/await
allows your .NET web apps to be able to handle more HTTP requests while waiting for IO to complete.
For Desktop/App Developers
But desktop apps don’t handle HTTP requests…
Well, desktop apps do handle user input (keyboard, mouse, etc.), animations, etc. And there’s only one UI thread to do that.
User Input Is User Input
If we consider that HTTP requests in a web app are just user input, and desktop (or app) keyboard and mouse events are just user input - then it’s actually worse for desktop/app developers! They only get one thread to handle user input!
The I/O issues and fixes still apply.
However, the issue of CPU intensive tasks is another concern. In a nutshell, these types of tasks should not be done on the UI thread.
The types of tasks would include:
- Processing a large number of items in a loop
- Computing aggregations for reporting
If your app does this (on the main/UI thread), then there’s nothing to handle user input and UI stuff like animations, etc.
This leads to freezing and laggy apps.
The solution is to offload CPU intensive tasks to a background task/thread. This starts to get into queuing up new threads/tasks, how to use ConfigureAwait(false)
to keep asynchronous branches of your code on a non-UI context, etc. All things beyond the scope of our article.
The Async Keyword
Let’s start looking at the async/await
keywords and how they are to be used.
There’s confusion over the async keyword. Why? Because it looks like it makes your method asynchronous. But, it doesn’t.
That’s confusing. The async
keyword doesn’t make my method asynchronous? Yep.
What Does it Do Then?
All the async
keyword does is enable the await keyword. That’s it. That’s all. It does nothing else.
So, just think of the async
keyword as the enableAwait
keyword.
The Await Keyword
The await
keyword is where the magic happens. It basically says (to the reader of the code):
I (the thread) will make sure if something asynchronous happens under here, that I’ll go do something else (like handle HTTP requests). Some thread in the future will come back here once the asynchronous stuff is done.
Generally, the most common usage of await
is when you are doing I/O - like getting results from a database query or getting contents from a file.
When you await
a method that does I/O, it’s not your app that does the I/O - it’s ultimately the operating system. So your thread is just sitting there…waiting…
await
will tell the current thread to just go away and do something useful. Let the operating system and the .NET framework get another thread later - whenever it needs one.
Consider this as a visual guide:
var result1 = await SomeAsyncIO1(); // OS is doing I/O while thread will go do something else.
// A thread gets the results.
var result2 = await SomeAsyncIO2(result1); // Thread goes to do something else.
// One comes back...
await SomeAsyncIO3(result2); // Goes away again...
// Comes back to finish the method.
You might ask yourself at this point:
If the
async
keyword doesn’t make a method asynchronous then what does?
What Makes A Method Asynchronous Then?
Well - it’s not the async
keyword as we learned. Go figure.
Any method that returns an “awaitable” - Task
or Task<T>
can be awaited using the await
keyword.
There are actually other awaitables types. And, an “awaitable” method doesn’t strictly have to be an asynchronous method. But ignore that - this is meant to be for “the rest of us.”
For the purpose of this article, we’ll assume that an “asynchronous method” is a method that returns Task
or Task<T>
.
When Does This Happen?
When will we ever need to return a Task
from a method? It’s usually when doing I/O. Most I/O libraries or built-in .NET APIs will have an “Async” version of a method.
For example, the SqlConnection
class has an Open
method that will begin the connection. But, it also has an OpenAsync
method. It also has an ExecuteNonQueryAsync
method.
public async Task IssueSqlCommandAsync() {
using(var con = new SqlConnection(_connectionString))
{
// Some code to create an sql command "sqlCommand" would be here...
await con.OpenAsync();
return await sqlCommand.ExecuteNonQueryAsync();
}
}
What makes the OpenAsync
and ExecuteNonQueryAsync
methods asynchronous is not the async
keyword, but it is that they return a Task
or Task<T>
.
Async All The Way Down
It is possible to do something like this (notice the lack of async
and await
):
public Task GetSomeData() {
return DoSomethingAsync();
}
And then await
that method:
// Inside some other method....
await GetSomeData();
GetSomeData
doesn’t await the call to DoSomethingAsync
- it just returns the Task
. Remember that await
doesn’t care if a method is using the async
keyword - it just requires that the method return a Task
.
It is possible to do this - create a method that calls an asynchronous method but doesn’t await.
It’s A Best Practice
However, this is considered a bad practice. Why?
Since this article is supposed to be to the point and practical:
Using async/await “all the way down” simply captures exceptions in asynchronous methods better.
If you mark every method that returns a Task
with the async
keyword - which in turn enables the await
keyword - it handles exceptions better and makes them understandable when looking at the exception’s message and stack trace.
Conclusion
To summarize briefly:
The
async
keyword doesn’t make methods asynchronous - it simply enables theawait
keyword.If a method returns a
Task
orTask<T>
then it can be used by theawait
keyword to manage the asynchronous details of our code.Doing I/O always results in blocking threads. This means your web apps can’t process as many HTTP requests in parallel and freezing and laggy apps.
Using
async/await
helps us create code that will allow our threads to stop blocking and do useful work while performing I/O.This leads to web apps that can handle more requests per second and apps that are more responsive for their users.
I hope this is an understandable introduction to async/await
. It’s not an easy topic - and as always - gaining experience by using this feature will, over time, help us to understand what’s going on.
There’s so much more to be said and so many more concepts surrounding async/await
. Some include:
- What is a
SynchronizationContext
? When should I be aware of this? - What about .NET Core vs .NET Framework - is there a difference I should be aware of?
- Why can I mark a method as
async void
? What does this do? Should I do this? - How do I offload CPU intensive work to a background thread/task?
- Is it possible to do work on a background thread and return to the UI thread at the very end?
- How do I call a method marked with the
async
keyword from synchronous code? What happens when I do this?
New Site!
I’ll be writing new content over at my new site/blog. Check it out!