Tasks, async and await
A Task is work that finishes later. An async method can await a task, which frees its thread while it waits for a database or the network, so a web server can keep serving other requests. Await every async call, and name async methods with Async.
Journeyman · C# · ASP.NET Core · Updated
After this lesson you can
- Write an async method that awaits other async methods and returns Task or Task<T>
- Explain why a web API awaits database and network calls instead of blocking a thread
Key takeaways
- A
Taskis work that finishes later, and aTask<T>finishes with aT. By convention, methods that return tasks end inAsync; controller actions don't. awaitgets a task's result without blocking the thread, so a web server can serve other requests while the database works.- An
asyncmethod returnsTaskorTask<T>, but itsreturngives just theT. - Start several tasks first, then
await Task.WhenAll(...)to wait for them all at once. - Await every task and stay async all the way: never block on a task with
.Resultor.Wait().
Think of a waiter in a busy restaurant. They take a table's order to the kitchen. A bad waiter stands at the kitchen door until the food is ready, while every other table waits. A good waiter hands in the order, serves other tables, and comes back when the kitchen calls.
A web server has the same choice, many times a second. A request asks the database for categories. The database takes a few milliseconds, an eternity for a computer. Should the thread handling that request (the waiter) stand and wait, or go and serve another request until the answer arrives? With async and await, it goes and serves.
Tasks
A Task is a piece of work that will finish later. A method that starts slow work, such as a query or a download, can return a task straight away instead of making its caller wait:
| Return type | Means |
|---|---|
Task |
"I'll finish later, with nothing to give you." |
Task<int> |
"I'll finish later, and give you an int." |
Task<List<Category>> |
"I'll finish later, and give you a list of categories." |
By convention, methods that return tasks end in Async: QueryAsync, SaveChangesAsync, ReadAllTextAsync.
await
await waits for a task to finish and gives you its result, without blocking the thread while it waits:
Console.WriteLine("Ordering...");
string meal = await CookAsync("pancakes");
Console.WriteLine( quot;Served: {meal}");
async Task<string> CookAsync(string dish)
{
await Task.Delay(500);
return quot;hot {dish}";
}
Ordering...
Served: hot pancakes
Task.Delay(500)is a task that finishes after half a second: a stand-in for a slow database.asyncon a method allowsawaitinside it. An async method returnsTaskorTask<T>, but itsreturngives just theT:returnreturns aquot;hot {dish}"; string, and C# wraps it in theTask<string>.await CookAsync(...)gets thestringout of theTask<string>.
Read top to bottom, the code looks just like ordinary code, and it runs in the same order. The difference is invisible: during each await, the thread was free to do other work.
Starting several tasks at once
A task starts running as soon as it's created. So you can start several, then await them together, the way a waiter puts in three orders before collecting any:
using System.Diagnostics;
Stopwatch timer = Stopwatch.StartNew();
Task<string> pancakes = CookAsync("pancakes");
Task<string> eggs = CookAsync("eggs");
Task<string> toast = CookAsync("toast");
string[] meals = await Task.WhenAll(pancakes, eggs, toast);
Console.WriteLine(string.Join(", ", meals));
Console.WriteLine(timer.ElapsedMilliseconds < 1000 ? "Under a second" : "Over a second");
async Task<string> CookAsync(string dish)
{
await Task.Delay(500);
return quot;hot {dish}";
}
hot pancakes, hot eggs, hot toast
Under a second
Three half-second dishes in about half a second, because they cooked at the same time. Task.WhenAll waits for all of them, and gives their results in order.
Async in a web API
ASP.NET Core actions can be async, and Dapper has async versions of every method. Here's GetById from the DAO to the controller:
public async Task<Category?> GetByIdAsync(int categoryId)
{
using SqlConnection connection = new SqlConnection(connectionString);
return await connection.QuerySingleOrDefaultAsync<Category>(
"SELECT CategoryId, CategoryName, Description FROM Categories WHERE CategoryId = @CategoryId;",
new { CategoryId = categoryId });
}
[HttpGet("{id}")]
public async Task<ActionResult<Category>> GetById(int id)
{
Category? category = await categoryDao.GetByIdAsync(id);
if (category == null)
{
return NotFound();
}
return category;
}
The changes are mechanical:
asyncon the method.- The return type wrapped in
Task<...>:Category?becomesTask<Category?>,ActionResult<Category>becomesTask<ActionResult<Category>>,IActionResultbecomesTask<IActionResult>. - The async version of each call, with
await:QuerySingleOrDefaultAsync,ExecuteAsync,QueryAsync. Asyncon the DAO methods' names, and in the interface:Task<Category?> GetByIdAsync(int categoryId);.
Controller actions keep their names (GetById, not GetByIdAsync). ASP.NET Core drops an Async ending from action names anyway, so CreatedAtAction(nameof(GetByIdAsync), ...) would look for an action that doesn't exist.
While the database works on the query, the request's thread goes back to ASP.NET Core to serve other requests. On a busy server that's the difference between handling hundreds of requests at once and running out of threads. That's why professional ASP.NET Core code is async from the controller all the way down to the database.
Rules of thumb
- Await every task. A task you don't await may fail silently, and the method carries on before the work is done.
- Async all the way. Once one method is async, its callers should be too. Never block on a task with
.Resultor.Wait(): that holds the thread hostage, which is exactly what async was meant to avoid. async voidis only for event handlers. Everywhere else, returnTask.
Check yourself
2 questions to try on the lesson page.
So what
await lets a thread serve others while it waits for slow work, and the code still reads top to bottom. Make the DAO async, the controller async, and await every call between them.