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 adecimalin the end, a price for example. ATaskwith no type only says "done".- A method that waits for something is marked with
asyncand returns a task. By convention, its name ends inAsync. awaitin 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);Asking for the price...Price: €24.90async Task<decimal>: the method is asynchronous and gives adecimalin the end. In the body you writereturn 24.90m, not areturnof a task: the compiler makes the task.- You can write
awaitonly in anasyncmethod or in the program's top-level code. await Warehouse.GetPriceAsync(…)gives you thedecimaldirectly. The task gets "unwrapped".
More precisely, if you are curious
- With the
class Programwrapper, theMainmethod becomes asynchronous too:static async Task Main(). Top-level code withawaitis a shorthand for exactly this.
class Program{ static async Task Main() { decimal price = await Warehouse.GetPriceAsync("Peppermint"); Console.WriteLine("Price: €" + price); }}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.Delayonly 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;}The async method starts right away and runs until the first await on something unfinished: it prints "Asking the warehouse". There it returns the task, so "Waiting for the price" comes next. await priceTask waits 300 ms, then the method carries on with "The warehouse answered" and returns the price.
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);}Espresso beans: €24.90Sencha green tea: €9.80Peppermint: €4.20If 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]);}Espresso beans: €24.90Sencha green tea: €9.80Peppermint: €4.20Select 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
- Three products. The warehouse answers each request after 300 ms.
- await pauses the loop until the warehouse answers for "Espresso beans". The next request has not started yet.
- await pauses the loop until the warehouse answers for "Sencha green tea". The next request has not started yet.
- await pauses the loop until the warehouse answers for "Peppermint". The next request has not started yet.
- One after another: 3 × 300 = 900 ms.
- Select starts all three requests at once. Each one returns a Task right away, without waiting.
- await Task.WhenAll waits for all the tasks. The three waits run at the same time.
- 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
WhenAllonly waits, and you get the results from each task withawait. The task is already done, so thisawaitdoes 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;}Peppermint: €4.20, 14 in stock- Trap:
.Resultand.Wait().task.Resultgives the result, andtask.Wait()waits, but both block the program until the task finishes. You lose the benefit ofasync, 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 anAggregateException. Writeawait. - Trap:
async void. Anasync voidmethod does not return a task, so nobody can wait for it, and its exception cannot be caught with atryaround the call. Writeasync Task.async voidis 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);Task.WhenAll(tasks) finishes when all the tasks finish. With await, it gives an array with the results in the order of the 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");}No such productMore precisely, if you are curious
- If several tasks fail in
Task.WhenAll,awaitthrows the first exception. The others are intask.Exceptionof each task. The tasks that did not fail have a result, so you can wait for them one by one, each in its owntry(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 inHttpClientand 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.
The call to GetPriceAsync does not throw anything: it only returns the task, and the exception is kept inside it. It comes out on line 11, at await task, and there is no try there anymore. The fix is to put the await inside the try: decimal price = await Warehouse.GetPriceAsync("Cocoa");.
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:
One after another: Espresso beans: €24.90 Sencha green tea: €9.80 Peppermint: €4.20At the same time: Espresso beans: €24.90 Sencha green tea: €9.80 Peppermint: €4.20Total: €38.902. 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:
Peppermint: €4.20Cocoa: not in the warehouseEspresso beans: €24.90To pay: €29.10Test