async y await
En resumen: async y await en C# esperan un trabajo largo, como una petición, sin detener el programa: una Task es el trabajo que terminará más tarde, y Task.WhenAll espera varias a la vez.
Problema
Tres peticiones de 300 ms
La página de un producto en la tienda online le pregunta el precio al almacén. El almacén es otro programa y responde en 300 ms. Si el programa se queda esperando de brazos cruzados, en esos 300 ms no hace nada más. Un servidor web con 1000 usuarios que espera así por cada uno se detiene: todos hacen cola detrás de un almacén lento.
Un carrito con tres productos es todavía peor. Las tres peticiones una tras otra suman 900 ms, cuando el almacén podría responder a las tres a la vez en 300 ms.
La idea
Una tarea en lugar de esperar
Cuando pides en la caja de una cafetería, no te quedas en el mostrador hasta que te preparan el café. Tomas un número y haces otra cosa, y cuando el café está listo, lo recoges.
En C#, el número es una Task (tarea): un objeto para un trabajo que terminará más tarde. Task.Run y .Result los viste en la lección «Hilos»: allí .Result espera manteniendo ocupado el hilo. Aquí vamos a esperar sin retenerlo.
Task<decimal>es una tarea que al final dará undecimal, por ejemplo un precio. UnaTasksin tipo solo dice «listo».- El método que espera algo se marca con
asyncy devuelve una tarea. Por convención, su nombre termina enAsync. awaitdelante de una tarea significa «espera el resultado, pero sin detener el programa». Mientras la tarea no está lista, el programa puede hacer otro trabajo, por ejemplo atender al siguiente cliente.
Míralo
async y await
El almacén es una clase con un método. Task.Delay(300) es una tarea que termina después de 300 ms. Aquí hace de almacén lento:
class Warehouse{ static Dictionary<string, decimal> prices = new Dictionary<string, decimal> { { "Café espresso en grano", 24.90m }, { "Té verde sencha", 9.80m }, { "Menta", 4.20m }, };
public static async Task<decimal> GetPriceAsync(string product) { await Task.Delay(300); return prices[product]; }}El programa pregunta el precio y lo espera con await:
Console.WriteLine("Pregunto el precio...");decimal price = await Warehouse.GetPriceAsync("Café espresso en grano");Console.WriteLine("Precio: " + price + " €");Pregunto el precio...Precio: 24.90 €async Task<decimal>: el método es asíncrono y al final da undecimal. En el cuerpo escribesreturn 24.90m, no unreturnde una tarea: la tarea la crea el compilador.awaitsolo se puede escribir en un métodoasynco en el código top-level del programa.await Warehouse.GetPriceAsync(…)te da directamente eldecimal. La tarea se «desempaqueta».
Más precisamente, para curiosos
- Con la envoltura
class Program, el métodoMaintambién se vuelve asíncrono:static async Task Main(). El código top-level conawaites una forma abreviada de exactamente esto.
class Program{ static async Task Main() { decimal price = await Warehouse.GetPriceAsync("Menta"); Console.WriteLine("Precio: " + price + " €"); }}Precio: 4.20 €- El trabajo real que se espera es la red, el disco o una base de datos:
HttpClient(la lección «Un servidor web con Minimal API»),File.ReadAllTextAsync, consultas a una base de datos. AquíTask.Delaysolo lo sustituye, para que el código funcione sin red.
Pruébalo
En qué orden
¿Qué imprimirá el programa?
Console.WriteLine("Pedido recibido");Task<decimal> priceTask = GetPriceAsync();Console.WriteLine("Espero el precio");decimal price = await priceTask;Console.WriteLine("Precio: " + price + " €");
static async Task<decimal> GetPriceAsync(){ Console.WriteLine("Pregunto al almacén"); await Task.Delay(300); Console.WriteLine("El almacén respondió"); return 24.90m;}El método async empieza enseguida y avanza hasta el primer await sobre algo que no ha terminado: imprime «Pregunto al almacén». Ahí devuelve la tarea, por eso sigue «Espero el precio». await priceTask espera 300 ms, después el método continúa con «El almacén respondió» y devuelve el precio.
Míralo
Una tras otra o a la vez
El carrito tiene tres productos:
List<string> products = new List<string> { "Café espresso en grano", "Té verde sencha", "Menta" };Con await dentro del bucle, cada petición espera a la anterior:
// Una tras otra: unos 900 ms.foreach (string product in products){ decimal price = await Warehouse.GetPriceAsync(product); Console.WriteLine(product + ": " + price + " €");}Café espresso en grano: 24.90 €Té verde sencha: 9.80 €Menta: 4.20 €Si primero lanzas las tres tareas y solo después esperas, avanzan a la vez. Task.WhenAll es una tarea que termina cuando terminan todas, y da sus resultados en un array, en el orden de las tareas:
// A la vez: unos 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] + " €");}Café espresso en grano: 24.90 €Té verde sencha: 9.80 €Menta: 4.20 €Select de la lección de LINQ llama a GetPriceAsync para cada producto. El método no espera, sino que devuelve una tarea, así que tasks es una lista de tres tareas que ya están en marcha. La salida es la misma, pero la espera dura lo que la petición más lenta, unos 300 ms en lugar de 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] + " €");}Paso a paso, en palabras
- Tres productos. El almacén responde a cada petición después de 300 ms.
- await detiene el bucle hasta que el almacén responde por «Café espresso en grano». La siguiente petición todavía no ha salido.
- await detiene el bucle hasta que el almacén responde por «Té verde sencha». La siguiente petición todavía no ha salido.
- await detiene el bucle hasta que el almacén responde por «Menta». La siguiente petición todavía no ha salido.
- Una tras otra: 3 × 300 = 900 ms.
- Select lanza las tres peticiones a la vez. Cada una devuelve una Task enseguida, sin esperar.
- await Task.WhenAll espera todas las tareas. Las tres esperas avanzan al mismo tiempo.
- Después de 300 ms los tres precios están listos: 300 ms en lugar de 900. La salida es la misma.
Más precisamente, para curiosos
- Las tareas pueden ser de tipos distintos. Entonces
WhenAllsolo espera, y los resultados los obtienes de cada tarea conawait. La tarea ya está lista, así que esteawaitno espera:
Task<decimal> priceTask = Warehouse.GetPriceAsync("Menta");Task<int> stockTask = GetStockAsync("Menta");await Task.WhenAll(priceTask, stockTask);Console.WriteLine("Menta: " + await priceTask + " €, " + await stockTask + " uds.");
static async Task<int> GetStockAsync(string product){ await Task.Delay(300); return 14;}Menta: 4.20 €, 14 uds.- Trampa:
.Resulty.Wait().task.Resultda el resultado ytask.Wait()espera, pero los dos bloquean el programa hasta que la tarea termina. Pierdes la ventaja deasync, y en una aplicación con ventanas (WPF) o en un proyecto ASP.NET antiguo el programa puede quedarse colgado para siempre: espera una tarea que lo espera a él. Además, la excepción llega envuelta en unaAggregateException. Escribeawait. - Trampa:
async void. Un métodoasync voidno devuelve una tarea, así que nadie puede esperarlo, y su excepción no se captura con untryalrededor de la llamada. Escribeasync Task.async voides solo para los manejadores de eventos (la lección de WPF).
Pruébalo
Espera a todas
¿Qué método espera todas las tareas de la lista y da sus resultados?
decimal[] prices = await Task.(tasks);Task.WhenAll(tasks) termina cuando terminan todas las tareas. Con await da un array con los resultados en el orden de las tareas.
Míralo
Cuando el almacén dice que no
En el almacén no hay cacao. prices[product] lanza KeyNotFoundException, como en la lección de diccionarios. Una excepción en un método async se guarda en la tarea y sale en el await. Por eso la capturas como estás acostumbrado, con un try alrededor del await:
try{ decimal price = await Warehouse.GetPriceAsync("Cacao"); Console.WriteLine("Precio: " + price + " €");}catch (KeyNotFoundException){ Console.WriteLine("No existe ese producto");}No existe ese productoMás precisamente, para curiosos
- Si con
Task.WhenAllfallan varias tareas,awaitlanza la primera excepción. Las demás están entask.Exceptionde cada tarea. Las tareas que no han fallado tienen un resultado, así que puedes esperarlas una por una, cada una en su propiotry(la tarea 2 del ejercicio). - Si una petición puede tardar demasiado, el código real la cancela pasado un tiempo con un
CancellationToken. Lo verás enHttpClienty en el servidor web.
Pruébalo
El error que no se capturó
El programa debería imprimir «No existe ese producto», pero se detiene con KeyNotFoundException. ¿En qué línea está el problema?
Elige la línea con el error.
La llamada a GetPriceAsync no lanza nada: solo devuelve la tarea, y la excepción se guarda dentro de ella. Sale en la línea 11, en await task, y ahí ya no hay ningún try. La corrección es poner el await dentro del try: decimal price = await Warehouse.GetPriceAsync("Cacao");.
Ejercicio
Los precios del pedido
Resuelve las tareas en tu editor. Usa la clase Warehouse de la lección.
1. En secuencia y a la vez. Obtén los precios de los tres productos una vez uno tras otro y otra vez con Task.WhenAll, y después imprime el total. Pon // unos 900 ms y // unos 300 ms como comentarios debajo de las dos formas. Si quieres medirlo, System.Diagnostics.Stopwatch.StartNew() pone en marcha un cronómetro, y ElapsedMilliseconds dice cuánto tiempo ha pasado:
Una tras otra: Café espresso en grano: 24.90 € Té verde sencha: 9.80 € Menta: 4.20 €A la vez: Café espresso en grano: 24.90 € Té verde sencha: 9.80 € Menta: 4.20 €Total: 38.90 €2. Una petición rechazada. El pedido es menta, cacao y café espresso. Lanza las tres peticiones a la vez y después espera cada una en su propio try. Para el producto que falta, imprime «no está en el almacén», y que el total sea solo el de los productos disponibles:
Menta: 4.20 €Cacao: no está en el almacénCafé espresso en grano: 24.90 €A pagar: 29.10 €Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: static: lo que comparten todos los objetos · Excepciones · Hilos · Listas, diccionarios y LINQ
- Continúa en: Un servidor web con Minimal API · MVC: modelo, vista, controlador · Vistas con Razor · WebAPI: datos para otros programas
- Ejercicios sobre el tema: Web y escritorio