Ir al contenido
wannacode

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á un decimal, por ejemplo un precio. Una Task sin tipo solo dice «listo».
  • El método que espera algo se marca con async y devuelve una tarea. Por convención, su nombre termina en Async.
  • await delante 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 + " €");
Salida
Pregunto el precio...
Precio: 24.90 €
  • async Task<decimal>: el método es asíncrono y al final da un decimal. En el cuerpo escribes return 24.90m, no un return de una tarea: la tarea la crea el compilador.
  • await solo se puede escribir en un método async o en el código top-level del programa.
  • await Warehouse.GetPriceAsync(…) te da directamente el decimal. La tarea se «desempaqueta».
Más precisamente, para curiosos
  • Con la envoltura class Program, el método Main también se vuelve asíncrono: static async Task Main(). El código top-level con await es una forma abreviada de exactamente esto.
class Program
{
static async Task Main()
{
decimal price = await Warehouse.GetPriceAsync("Menta");
Console.WriteLine("Precio: " + price + " €");
}
}
Salida
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.Delay solo 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;
}
Respuesta

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 + " €");
}
Salida
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] + " €");
}
Salida
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
  1. Tres productos. El almacén responde a cada petición después de 300 ms.
  2. await detiene el bucle hasta que el almacén responde por «Café espresso en grano». La siguiente petición todavía no ha salido.
  3. await detiene el bucle hasta que el almacén responde por «Té verde sencha». La siguiente petición todavía no ha salido.
  4. await detiene el bucle hasta que el almacén responde por «Menta». La siguiente petición todavía no ha salido.
  5. Una tras otra: 3 × 300 = 900 ms.
  6. Select lanza las tres peticiones a la vez. Cada una devuelve una Task enseguida, sin esperar.
  7. await Task.WhenAll espera todas las tareas. Las tres esperas avanzan al mismo tiempo.
  8. 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 WhenAll solo espera, y los resultados los obtienes de cada tarea con await. La tarea ya está lista, así que este await no 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;
}
Salida
Menta: 4.20 €, 14 uds.
  • Trampa: .Result y .Wait(). task.Result da el resultado y task.Wait() espera, pero los dos bloquean el programa hasta que la tarea termina. Pierdes la ventaja de async, 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 una AggregateException. Escribe await.
  • Trampa: async void. Un método async void no devuelve una tarea, así que nadie puede esperarlo, y su excepción no se captura con un try alrededor de la llamada. Escribe async Task. async void es 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);

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");
}
Salida
No existe ese producto
Más precisamente, para curiosos
  • Si con Task.WhenAll fallan varias tareas, await lanza la primera excepción. Las demás están en task.Exception de cada tarea. Las tareas que no han fallado tienen un resultado, así que puedes esperarlas una por una, cada una en su propio try (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 en HttpClient y 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.

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:

Salida
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:

Salida
Menta: 4.20 €
Cacao: no está en el almacén
Café espresso en grano: 24.90 €
A pagar: 29.10 €

Test

Ponte a prueba

1. ¿Qué devuelve un método async Task<decimal> GetPriceAsync() cuando lo llamas sin await?
2. Tres peticiones de 300 ms cada una se lanzan con Task.WhenAll. ¿Cuánto dura más o menos la espera?
3. Un método async lanza una excepción después de await Task.Delay(300). ¿Dónde la capturas?
4. ¿Por qué task.Result es una trampa?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·