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 Task is work that finishes later, and a Task<T> finishes with a T. By convention, methods that return tasks end in Async; controller actions don't.
  • await gets a task's result without blocking the thread, so a web server can serve other requests while the database works.
  • An async method returns Task or Task<T>, but its return gives just the T.
  • 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 .Result or .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.
  • async on a method allows await inside it. An async method returns Task or Task<T>, but its return gives just the T: return quot;hot {dish}"; returns a string, and C# wraps it in the Task<string>.
  • await CookAsync(...) gets the string out of the Task<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:

  1. async on the method.
  2. The return type wrapped in Task<...>: Category? becomes Task<Category?>, ActionResult<Category> becomes Task<ActionResult<Category>>, IActionResult becomes Task<IActionResult>.
  3. The async version of each call, with await: QuerySingleOrDefaultAsync, ExecuteAsync, QueryAsync.
  4. Async on 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 .Result or .Wait(): that holds the thread hostage, which is exactly what async was meant to avoid.
  • async void is only for event handlers. Everywhere else, return Task.

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.

Key terms

Cheat sheets