Skip to content
wannacode

async and await

Problem

Three requests of 300 ms

A product page in the online shop asks the warehouse for the price. The warehouse is another program, and it answers in 300 ms. If the program sits idle and waits, it does nothing else during those 300 ms. A web server with 1,000 users that waits like this for each one grinds to a halt: everyone queues up behind one slow warehouse.

A cart with three products is even worse. The three requests one after another take 900 ms, while the warehouse could answer all three at once in 300 ms.

The idea

A task instead of waiting

When you order at the counter of a café, you do not stand at the counter until your coffee is made. You take a ticket number and do something else, and when the coffee is ready, you pick it up.

In C#, the ticket is a Task: an object for work that will finish later.

  • Task<decimal> is a task that will give a decimal in the end, a price for example. A Task with no type only says "done".
  • A method that waits for something is marked with async and returns a task. By convention, its name ends in Async.
  • await in front of a task means "wait for the result, but without stopping the program". While the task is not ready, the program can do other work, such as taking the next customer.

See it

async and await

The warehouse is a class with one method. Task.Delay(300) is a task that finishes after 300 ms. Here it plays the slow warehouse:

class Warehouse
{
static Dictionary<string, decimal> prices = new Dictionary<string, decimal>
{
{ "Espresso beans", 24.90m },
{ "Sencha green tea", 9.80m },
{ "Peppermint", 4.20m },
};
public static async Task<decimal> GetPriceAsync(string product)
{
await Task.Delay(300);
return prices[product];
}
}

The program asks for the price and waits for it with await:

Console.WriteLine("Asking for the price...");
decimal price = await Warehouse.GetPriceAsync("Espresso beans");
Console.WriteLine("Price: €" + price);
Output
Asking for the price...
Price: €24.90
  • async Task<decimal>: the method is asynchronous and gives a decimal in the end. In the body you write return 24.90m, not a return of a task: the compiler makes the task.
  • You can write await only in an async method or in the program's top-level code.
  • await Warehouse.GetPriceAsync(…) gives you the decimal directly. The task gets "unwrapped".
More precisely, if you are curious
  • With the class Program wrapper, the Main method becomes asynchronous too: static async Task Main(). Top-level code with await is a shorthand for exactly this.
class Program
{
static async Task Main()
{
decimal price = await Warehouse.GetPriceAsync("Peppermint");
Console.WriteLine("Price: €" + price);
}
}
Output
Price: €4.20
  • The real work you wait for is the network, the disk or a database: HttpClient (the "Your first server" lesson), File.ReadAllTextAsync, database queries. Task.Delay only stands in for it here, so the code works without a network.

Try it

In what order

What will the program print?

Console.WriteLine("Order received");
Task<decimal> priceTask = GetPriceAsync();
Console.WriteLine("Waiting for the price");
decimal price = await priceTask;
Console.WriteLine("Price: €" + price);
static async Task<decimal> GetPriceAsync()
{
Console.WriteLine("Asking the warehouse");
await Task.Delay(300);
Console.WriteLine("The warehouse answered");
return 24.90m;
}
Answer

See it

One after another or at the same time

The cart has three products:

List<string> products = new List<string> { "Espresso beans", "Sencha green tea", "Peppermint" };

With await in the loop, each request waits for the previous one:

// One after another: about 900 ms.
foreach (string product in products)
{
decimal price = await Warehouse.GetPriceAsync(product);
Console.WriteLine(product + ": €" + price);
}
Output
Espresso beans: €24.90
Sencha green tea: €9.80
Peppermint: €4.20

If you start all three tasks first and only then wait, they run at the same time. Task.WhenAll is a task that finishes when all of them finish, and it gives their results in an array, in the order of the tasks:

// At the same time: about 300 ms.
List<Task<decimal>> tasks = products.Select(p => Warehouse.GetPriceAsync(p)).ToList();
decimal[] prices = await Task.WhenAll(tasks);
for (int i = 0; i < products.Count; i++)
{
Console.WriteLine(products[i] + ": €" + prices[i]);
}
Output
Espresso beans: €24.90
Sencha green tea: €9.80
Peppermint: €4.20

Select from the LINQ lesson calls GetPriceAsync for each product. The method does not wait but returns a task, so tasks is a list of three tasks that are already running. The output is the same, but the wait is as long as the slowest request, about 300 ms instead of 900.

foreach (string product in products)
{
decimal price = await Warehouse.GetPriceAsync(product);
Console.WriteLine(product + ": €" + price);
}
List<Task<decimal>> tasks = products.Select(p => Warehouse.GetPriceAsync(p)).ToList();
decimal[] prices = await Task.WhenAll(tasks);
for (int i = 0; i < products.Count; i++)
{
Console.WriteLine(products[i] + ": €" + prices[i]);
}
Step by step, in words
  1. Three products. The warehouse answers each request after 300 ms.
  2. await pauses the loop until the warehouse answers for "Espresso beans". The next request has not started yet.
  3. await pauses the loop until the warehouse answers for "Sencha green tea". The next request has not started yet.
  4. await pauses the loop until the warehouse answers for "Peppermint". The next request has not started yet.
  5. One after another: 3 × 300 = 900 ms.
  6. Select starts all three requests at once. Each one returns a Task right away, without waiting.
  7. await Task.WhenAll waits for all the tasks. The three waits run at the same time.
  8. After 300 ms all three prices are ready: 300 ms instead of 900. The output is the same.
More precisely, if you are curious
  • The tasks can be of different types. Then WhenAll only waits, and you get the results from each task with await. The task is already done, so this await does not wait:
Task<decimal> priceTask = Warehouse.GetPriceAsync("Peppermint");
Task<int> stockTask = GetStockAsync("Peppermint");
await Task.WhenAll(priceTask, stockTask);
Console.WriteLine("Peppermint: €" + await priceTask + ", " + await stockTask + " in stock");
static async Task<int> GetStockAsync(string product)
{
await Task.Delay(300);
return 14;
}
Output
Peppermint: €4.20, 14 in stock
  • Trap: .Result and .Wait(). task.Result gives the result, and task.Wait() waits, but both block the program until the task finishes. You lose the benefit of async, and in an app with windows (WPF) or in an old ASP.NET project the program can hang forever: it waits for a task that is waiting for it. On top of that, the exception comes wrapped in an AggregateException. Write await.
  • Trap: async void. An async void method does not return a task, so nobody can wait for it, and its exception cannot be caught with a try around the call. Write async Task. async void is only for event handlers (the WPF lesson).

Try it

Wait for all of them

Which method waits for all the tasks in the list and gives their results?

decimal[] prices = await Task.(tasks);

See it

When the warehouse says no

The warehouse has no cocoa. prices[product] throws KeyNotFoundException, as in the dictionaries lesson. An exception in an async method is kept in the task and comes out at await. So you catch it the way you are used to, with a try around the await:

try
{
decimal price = await Warehouse.GetPriceAsync("Cocoa");
Console.WriteLine("Price: €" + price);
}
catch (KeyNotFoundException)
{
Console.WriteLine("No such product");
}
Output
No such product
More precisely, if you are curious
  • If several tasks fail in Task.WhenAll, await throws the first exception. The others are in task.Exception of each task. The tasks that did not fail have a result, so you can wait for them one by one, each in its own try (task 2 of the exercise).
  • If a request could go on for too long, real code cancels it after a while with a CancellationToken. You will meet it in HttpClient and in the web server.

Try it

The error that was not caught

The program should print "No such product", but it stops with KeyNotFoundException. Which line is the problem on?

Pick the line with the bug.

Exercise

The prices of the order

Solve these in your editor. Use the Warehouse class from the lesson.

1. One after another and at the same time. Get the prices of the three products once one after another and once with Task.WhenAll, then print the total. Put // about 900 ms and // about 300 ms as comments under the two ways. If you want to measure it, System.Diagnostics.Stopwatch.StartNew() starts a stopwatch, and ElapsedMilliseconds tells you how much time has passed:

Output
One after another:
Espresso beans: €24.90
Sencha green tea: €9.80
Peppermint: €4.20
At the same time:
Espresso beans: €24.90
Sencha green tea: €9.80
Peppermint: €4.20
Total: €38.90

2. A refused request. The order is peppermint, cocoa and espresso beans. Start all three requests at the same time, then wait for each one in its own try. For the missing product, print "not in the warehouse", and make the total only for the products that are there:

Output
Peppermint: €4.20
Cocoa: not in the warehouse
Espresso beans: €24.90
To pay: €29.10

Test

Check yourself

1. What does a method async Task<decimal> GetPriceAsync() return when you call it without await?
2. Three requests of 300 ms each are started with Task.WhenAll. About how long is the wait?
3. An async method throws an exception after await Task.Delay(300). Where do you catch it?
4. Why is task.Result a trap?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·