Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
El modelo de programación asincrónica de tareas (TAP) proporciona una capa de abstracción sobre la codificación asincrónica típica. En este modelo, escribirá código como una secuencia de instrucciones, igual que de costumbre. La diferencia es que puede leer el código basado en tareas a medida que el compilador procesa cada instrucción y antes de empezar a procesar la siguiente instrucción. Para lograr este modelo, el compilador realiza muchas transformaciones para completar cada tarea. Algunas instrucciones pueden iniciar el trabajo y devolver un Task objeto que representa el trabajo en curso y el compilador debe resolver estas transformaciones. El objetivo de la programación asincrónica de tareas es habilitar el código que lee como una secuencia de instrucciones, pero se ejecuta en un orden más complicado. La ejecución se basa en la asignación de recursos externos y cuando se completan las tareas.
El modelo de programación asincrónica de tareas es análogo a cómo las personas proporcionan instrucciones para los procesos que incluyen tareas asincrónicas. En este artículo se usa un ejemplo con instrucciones para preparar el desayuno para mostrar cómo las async palabras clave y await facilitan la razón del código que incluye una serie de instrucciones asincrónicas. Las instrucciones para hacer un desayuno se pueden proporcionar como una lista:
- Verte una taza de café.
- Calentar una sartén y luego freír dos huevos.
- Cocine tres tortitas de patata.
- Tosta dos rebanadas de pan.
- Unta mantequilla y mermelada en la tostada.
- Vierta un vaso de jugo de naranja.
Si tiene experiencia con la cocina, es posible que complete estas instrucciones de forma asincrónica. Empiece a calentar la sartén y luego empiece a cocinar la masa de patata. Pones el pan en la tostadora y luego empiezas a cocinar los huevos. En cada paso del proceso, inicia una tarea y, a continuación, realiza la transición a otras tareas que están listas para su atención.
El desayuno de cocina es un buen ejemplo de trabajo asincrónico que no es paralelo. Una persona (o hilo) puede manejar todas las tareas. Una persona puede hacer el desayuno de forma asincrónica iniciando la siguiente tarea antes de que se complete la tarea anterior. Cada tarea de cocción progresa independientemente de si alguien está viendo activamente el proceso. En cuanto empieces a calentar la sartén para los huevos, puedes empezar a cocinar los hash browns. Después de que las croquetas de patata comiencen a cocinarse, puede poner el pan en la tostadora.
Para un algoritmo paralelo, necesitas varias personas que cocinen (o varios hilos). Una persona cocina los huevos, otra cocina las croquetas de patata, y así sucesivamente. Cada persona se centra en su tarea específica. Cada persona que está cocinando (o cada subproceso) se bloquea sincrónicamente esperando a que se complete la tarea actual: Tortitas de patata listo para voltear, pan listo para aparecer en tostadora, etc.
Considere la misma lista de instrucciones sincrónicas escritas como instrucciones de código de C#:
using System;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class HashBrown { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static void Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = FryEggs(2);
Console.WriteLine("eggs are ready");
HashBrown hashBrown = FryHashBrowns(3);
Console.WriteLine("hash browns are ready");
Toast toast = ToastBread(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static Toast ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
Task.Delay(3000).Wait();
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static HashBrown FryHashBrowns(int patties)
{
Console.WriteLine($"putting {patties} hash brown patties in the pan");
Console.WriteLine("cooking first side of hash browns...");
Task.Delay(3000).Wait();
for (int patty = 0; patty < patties; patty++)
{
Console.WriteLine("flipping a hash brown patty");
}
Console.WriteLine("cooking the second side of hash browns...");
Task.Delay(3000).Wait();
Console.WriteLine("Put hash browns on plate");
return new HashBrown();
}
private static Egg FryEggs(int howMany)
{
Console.WriteLine("Warming the egg pan...");
Task.Delay(3000).Wait();
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
Task.Delay(3000).Wait();
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
Si interpreta estas instrucciones como haría un ordenador, el desayuno tarda unos 30 minutos en prepararse. La duración es la suma de los tiempos de tarea individuales. El ordenador bloquea cada instrucción hasta que se completa todo el trabajo y, a continuación, continúa con la siguiente tarea. Este enfoque puede tardar mucho tiempo. En el ejemplo de desayuno, el método del ordenador crea un desayuno no satisfactorio. Las tareas posteriores de la lista sincrónica, como tostar el pan, no se inician hasta que se completen las tareas anteriores. Algunos alimentos se frían antes de que el desayuno esté listo para servir.
Si desea que el equipo ejecute instrucciones de forma asincrónica, debe escribir código asincrónico. Al escribir programas cliente, quiere que la interfaz de usuario responda a la entrada del usuario. La aplicación no debe inmovilizar toda la interacción al descargar datos de la web. Cuando escribes programas de servidor, no quieres bloquear hilos que podrían estar sirviendo otras peticiones. El uso de código sincrónico cuando existen alternativas asincrónicas daña la capacidad de escalar horizontalmente de forma menos costosa. Al final, los subprocesos bloqueados pasarán factura.
Las aplicaciones modernas correctas requieren código asincrónico. Sin soporte del lenguaje, la escritura de código asincrónico requiere callbacks, eventos de finalización u otros medios que pueden oscurecer la intención original del código. La ventaja del código sincrónico es la acción paso a paso que facilita el examen y la comprensión. Los modelos asincrónicos tradicionales le obligan a centrarse en la naturaleza asincrónica del código, no en las acciones fundamentales del código.
No bloquee, espere en su lugar
El código anterior resalta una práctica de programación desafortunada: Escritura de código sincrónico para realizar operaciones asincrónicas. El código impide que el subproceso actual realice cualquier otro trabajo. El código no interrumpe el subproceso mientras hay tareas en ejecución. El resultado de este modelo es similar a mirar en la tostadora después de poner el pan. Omite las interrupciones y no inicia otras tareas hasta que aparezca el pan. No sacas la mantequilla y la mermelada de la nevera. Podrías perderte ver un fuego comenzando en el fogón. Quieres tostar el pan y manejar otras tareas al mismo tiempo. Lo mismo sucede con tu código.
Puede empezar actualizando el código para que el subproceso no bloquee mientras se ejecutan las tareas. La palabra clave await proporciona una manera de no bloqueo para iniciar una tarea y luego continuar la ejecución cuando se completa la tarea. Una versión asincrónica simple del código de desayuno tiene el siguiente aspecto:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Egg eggs = await FryEggsAsync(2);
Console.WriteLine("eggs are ready");
HashBrown hashBrown = await FryHashBrownsAsync(3);
Console.WriteLine("hash browns are ready");
Toast toast = await ToastBreadAsync(2);
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
El código actualiza los cuerpos de método originales de FryEggs, FryHashBrownsy ToastBread para devolver Task<Egg>objetos , Task<HashBrown>y Task<Toast> , respectivamente. Los nombres de método actualizados incluyen el sufijo "Async": FryEggsAsync, FryHashBrownsAsyncy ToastBreadAsync. El Main método devuelve el Task objeto, aunque no tiene una return expresión, que es por diseño. Para obtener más información, consulte Evaluación de una función asincrónica que devuelve void.
Nota:
El código actualizado aún no aprovecha las características clave de la programación asincrónica, lo que puede dar lugar a tiempos de finalización más cortos. El código procesa las tareas aproximadamente la misma cantidad de tiempo que la versión sincrónica inicial. Para ver las implementaciones completas del método, consulte la versión final del código más adelante en este artículo.
Vamos a aplicar el ejemplo de desayuno al código actualizado. El hilo no se bloquea mientras los huevos o las tortitas de patata se están cocinando, pero el código tampoco inicia otras tareas hasta que se completa el trabajo actual. Todavía pones el pan en la tostadora y mira a la tostadora hasta que el pan aparezca, pero ahora puedes responder a interrupciones. En un restaurante donde se realizan varios pedidos, el cocinero puede comenzar un nuevo pedido mientras que otro ya está cocinando.
En el código actualizado, el subproceso que trabaja en la tarea del desayuno no se bloquea mientras espera a que se termine cualquier tarea iniciada que esté sin finalizar. Para algunas aplicaciones, este cambio es todo lo que necesita. Puede permitir que la aplicación admita la interacción del usuario mientras se descargan datos desde la web. En otros escenarios, es posible que desee iniciar otras tareas mientras espera a que se complete la tarea anterior.
Iniciar tareas simultáneamente
Para la mayoría de las operaciones, desea iniciar varias tareas independientes inmediatamente. A medida que completes cada tarea, inicias otros trabajos que estén listos para empezar. Al aplicar esta metodología al ejemplo de desayuno, puede preparar el desayuno más rápidamente. También tiene todo listo cerca de la misma hora, por lo que podrá disfrutar de un desayuno caliente.
La System.Threading.Tasks.Task clase y los tipos relacionados son clases que puede usar para aplicar este estilo de razonamiento a las tareas que están en curso. Este enfoque le permite escribir código que se parezca más a la forma en que se crea el desayuno en la vida real. Empiezas a cocinar los huevos, las patatas fritas ralladas y las tostadas al mismo tiempo. A medida que cada alimento requiere acción, diriges tu atención a esa tarea, realizas la acción y, a continuación, esperas a que haya algo más que requiera tu atención.
En tu código, inicias una tarea y retienes el objeto Task que representa el trabajo. Use el método await en la tarea para retrasar la acción en el trabajo hasta que el resultado esté listo.
Aplique estos cambios al código de desayuno. El primer paso es almacenar las tareas de las operaciones cuando se inician, en lugar de usar la await expresión :
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Console.WriteLine("Breakfast is ready!");
Estas revisiones no ayudan a preparar el desayuno más rápido. La await expresión se aplica a todas las tareas tan pronto como se inician. El siguiente paso es mover las expresiones de await para las tortitas de patata y los huevos al final del método, antes de servir el desayuno:
Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");
Task<Egg> eggsTask = FryEggsAsync(2);
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Breakfast is ready!");
Ahora tiene un desayuno preparado de forma asincrónica que tarda unos 20 minutos en prepararse. El tiempo total de cocción se reduce porque algunas tareas se ejecutan simultáneamente.
El código actualiza el proceso de preparación reduciendo el tiempo de cocción, pero introduce una regresión al quemar los huevos y los hash browns. Inicia todas las tareas asincrónicas a la vez. Y esperará por una tarea solo cuando necesite los resultados. El código puede ser similar al programa de una aplicación web que realiza solicitudes a diferentes microservicios y, a continuación, combina los resultados en una sola página. Realiza todas las solicitudes inmediatamente y, a continuación, aplica la await expresión en todas esas tareas y redacta la página web.
Soporte para la composición con tareas
Las revisiones de código anteriores ayudan a preparar todo para el desayuno al mismo tiempo, excepto la tostada. El proceso de hacer la tostada es una composición de una operación asincrónica (tostar el pan) con operaciones sincrónicas (untar mantequilla y mermelada en la tostada). En este ejemplo se muestra un concepto importante sobre la programación asincrónica:
Importante
La composición de una operación asincrónica seguida del trabajo sincrónico es una operación asincrónica. Se indicó otra manera, si alguna parte de una operación es asincrónica, toda la operación es asincrónica.
En las actualizaciones anteriores, aprendiste a utilizar objetos Task o Task<TResult> para retener tareas en ejecución. Espera en cada tarea antes de usar su resultado. El siguiente paso es crear métodos que representen la combinación de otro trabajo. Antes de servir el desayuno, querrá esperar en la tarea que representa tostar el pan antes de distribuir la mantequilla y la mermelada.
Puede representar este trabajo con el código siguiente:
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
El método MakeToastWithButterAndJamAsync tiene el modificador async en su firma que señala al compilador que el método contiene una expresión await y contiene operaciones asíncronas. Este método representa la tarea que tuesta el pan y, después, extiende la mantequilla y la mermelada. El método devuelve un Task<TResult> objeto que representa la composición de las tres operaciones.
El bloque principal revisado de código ahora tiene el siguiente aspecto:
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var hashBrownTask = FryHashBrownsAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var eggs = await eggsTask;
Console.WriteLine("eggs are ready");
var hashBrown = await hashBrownTask;
Console.WriteLine("hash browns are ready");
var toast = await toastTask;
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
Este cambio de código ilustra una técnica importante para trabajar con código asincrónico. Puedes componer tareas separando las operaciones en un método nuevo que devuelva una tarea. Puede elegir cuándo esperar para continuar con esa tarea. Puede iniciar otras tareas simultáneamente.
Control de excepciones asincrónicas
Hasta este punto, el código asume implícitamente que todas las tareas se completan correctamente. Los métodos asincrónicos lanzan excepciones, al igual que sus homólogos sincrónicos. Los objetivos de compatibilidad asincrónica con excepciones y control de errores son los mismos que para la compatibilidad asincrónica en general. El procedimiento recomendado es escribir código que lea como una serie de instrucciones sincrónicas. Las tareas lanzan excepciones cuando no se pueden completar exitosamente. El código de cliente puede detectar esas excepciones cuando la await expresión se aplica a una tarea iniciada.
En el ejemplo del desayuno, supongamos que la tostadora se activa mientras tosta el pan. Puede simular ese problema modificando el ToastBreadAsync método para que coincida con el código siguiente:
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(2000);
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
await Task.Delay(1000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
Nota:
Al compilar este código, verá una advertencia sobre el código inaccesible. Se trata de un error por diseño. Después de que el tostador se active, las operaciones no continúan normalmente y el código devuelve un error.
Después de realizar los cambios en el código, ejecute la aplicación y compruebe la salida:
Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 hash brown patties in the pan
Cooking first side of hash browns...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a hash brown patty
Flipping a hash brown patty
Flipping a hash brown patty
Cooking the second side of hash browns...
Cracking 2 eggs
Cooking the eggs ...
Put hash browns on plate
Put eggs on plate
Eggs are ready
Hash browns are ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
at AsyncBreakfast.Program.<Main>(String[] args)
Tenga en cuenta que bastantes tareas finalizan entre el momento en que el tostador se incendia y el sistema observa la excepción. Cuando una tarea que se ejecuta de forma asincrónica produce una excepción, se produce un error en esa tarea. El Task objeto contiene la excepción lanzada en la propiedad Task.Exception. Las tareas con errores producen una excepción cuando la await expresión se aplica a la tarea.
Hay dos mecanismos importantes para comprender este proceso:
- Cómo se almacena una excepción en una tarea con errores
- Cómo se desempaqueta y se vuelve a iniciar una excepción cuando el código espera (
await) en una tarea con errores
Cuando el código que se ejecuta de forma asincrónica produce una excepción, la excepción se almacena en el Task objeto . La propiedad Task.Exception es un objeto System.AggregateException porque pueden producirse más de una excepción durante el trabajo asincrónico. Cualquier excepción lanzada se agrega a la colección AggregateException.InnerExceptions. Si la Exception propiedad es null, se crea un nuevo AggregateException objeto y la excepción iniciada es el primer elemento de la colección.
El escenario más común para una tarea con errores es que la Exception propiedad contiene exactamente una excepción. Cuando su código espera en una tarea con error, vuelva a lanzar la primera excepción AggregateException.InnerExceptions de la colección. Este resultado es la razón por la que la salida del ejemplo muestra un System.InvalidOperationException objeto en lugar de un AggregateException objeto. La extracción de la primera excepción interna hace que el trabajo con métodos asincrónicos sea lo más parecido posible a trabajar con sus homólogos sincrónicos. Puede examinar la propiedad Exception en su código cuando su escenario podría generar varias excepciones.
Sugerencia
La práctica recomendada es que las excepciones de validación de argumentos surjan sincrónicamente de los métodos que devuelven tareas. Para obtener más información y ejemplos, vea Excepciones en métodos que devuelven tareas.
Antes de continuar con la sección siguiente, comente las dos instrucciones siguientes en el método ToastBreadAsync. No quieres iniciar otro incendio:
Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");
Aplicar expresiones await a tareas de forma eficaz
Puede mejorar la serie de await expresiones al final del código anterior mediante métodos de la Task clase . Una API es el WhenAll método , que devuelve un Task objeto que se completa cuando se completan todas las tareas de su lista de argumentos. El código siguiente muestra este método:
await Task.WhenAll(eggsTask, hashBrownTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");
Otra opción es usar el WhenAny método , que devuelve un Task<Task> objeto que se completa cuando se completa cualquiera de sus argumentos. Puede esperar la tarea devuelta porque sabe que la tarea está terminada. En el código siguiente se muestra cómo puede usar el WhenAny método para esperar a que finalice la primera tarea y, a continuación, procesar su resultado. Después de procesar el resultado de la tarea completada, quite la tarea completada de la lista de tareas pasadas al WhenAny método .
var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("Eggs are ready");
}
else if (finishedTask == hashBrownTask)
{
Console.WriteLine("Hash browns are ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("Toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Cerca del final del fragmento de código, observe la await finishedTask; expresión. Esta línea es importante porque Task.WhenAny devuelve una Task<Task> tarea contenedora que contiene la tarea completada. Cuando , await Task.WhenAnyestá esperando a que se complete la tarea contenedora y el resultado es la tarea real que finalizó primero. Sin embargo, para recuperar el resultado de esa tarea o asegurarse de que se produzcan correctamente las excepciones, debe realizar await la propia tarea completada (almacenada en finishedTask). Aunque sepa que la tarea ha finalizado, a la espera de nuevo le permite acceder a su resultado o controlar las excepciones que podrían haber provocado un error.
Revisión del código final
Este es el aspecto de la versión final del código:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AsyncBreakfast
{
// These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
internal class HashBrown { }
internal class Coffee { }
internal class Egg { }
internal class Juice { }
internal class Toast { }
class Program
{
static async Task Main(string[] args)
{
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
var eggsTask = FryEggsAsync(2);
var hashBrownTask = FryHashBrownsAsync(3);
var toastTask = MakeToastWithButterAndJamAsync(2);
var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
Console.WriteLine("eggs are ready");
}
else if (finishedTask == hashBrownTask)
{
Console.WriteLine("hash browns are ready");
}
else if (finishedTask == toastTask)
{
Console.WriteLine("toast is ready");
}
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Juice oj = PourOJ();
Console.WriteLine("oj is ready");
Console.WriteLine("Breakfast is ready!");
}
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
private static Juice PourOJ()
{
Console.WriteLine("Pouring orange juice");
return new Juice();
}
private static void ApplyJam(Toast toast) =>
Console.WriteLine("Putting jam on the toast");
private static void ApplyButter(Toast toast) =>
Console.WriteLine("Putting butter on the toast");
private static async Task<Toast> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Console.WriteLine("Putting a slice of bread in the toaster");
}
Console.WriteLine("Start toasting...");
await Task.Delay(3000);
Console.WriteLine("Remove toast from toaster");
return new Toast();
}
private static async Task<HashBrown> FryHashBrownsAsync(int patties)
{
Console.WriteLine($"putting {patties} hash brown patties in the pan");
Console.WriteLine("cooking first side of hash browns...");
await Task.Delay(3000);
for (int patty = 0; patty < patties; patty++)
{
Console.WriteLine("flipping a hash brown patty");
}
Console.WriteLine("cooking the second side of hash browns...");
await Task.Delay(3000);
Console.WriteLine("Put hash browns on plate");
return new HashBrown();
}
private static async Task<Egg> FryEggsAsync(int howMany)
{
Console.WriteLine("Warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine($"cracking {howMany} eggs");
Console.WriteLine("cooking the eggs ...");
await Task.Delay(3000);
Console.WriteLine("Put eggs on plate");
return new Egg();
}
private static Coffee PourCoffee()
{
Console.WriteLine("Pouring coffee");
return new Coffee();
}
}
}
El código completa las tareas de desayuno asincrónicas en unos 15 minutos. El tiempo total se reduce porque algunas tareas se ejecutan simultáneamente. El código supervisa simultáneamente varias tareas y realiza acciones solo según sea necesario.
El código final es asincrónico. Refleja con más precisión cómo una persona puede cocinar el desayuno. Compare el código final con el primer ejemplo de código del artículo. Las acciones principales siguen siendo claras leyendo el código. Puede leer el código final de la misma manera que leyó la lista de instrucciones para hacer un desayuno, como se muestra al principio del artículo. Las características del lenguaje para las palabras clave async y await proporcionan la traducción que cada persona realiza para seguir las instrucciones escritas: Inicia las tareas tan pronto como puedas y no te bloquees mientras esperas a que se completen.
Async/await frente a ContinueWith
Las palabras clave async y await simplifican la sintaxis en comparación con el uso directo de Task.ContinueWith. Aunque async/await y ContinueWith tienen una semántica similar para controlar las operaciones asincrónicas, el compilador no traduce necesariamente await directamente en llamadas al método ContinueWith. En su lugar, el compilador genera código de máquina de estado optimizado que proporciona el mismo comportamiento lógico. Esta transformación proporciona ventajas significativas de legibilidad y mantenimiento, especialmente al encadenar varias operaciones asincrónicas.
Considere un escenario en el que debe realizar varias operaciones asincrónicas secuenciales. Este es el aspecto de la misma lógica cuando se implementa con ContinueWith en comparación con async/await.
Uso de ContinueWith
Con ContinueWith, cada paso de una secuencia de operaciones asincrónicas requiere continuaciones anidadas:
// Using ContinueWith - demonstrates the complexity when chaining operations
static Task MakeBreakfastWithContinueWith()
{
return StartCookingEggsAsync()
.ContinueWith(eggsTask =>
{
var eggs = eggsTask.Result;
Console.WriteLine("Eggs ready, starting bacon...");
return StartCookingBaconAsync();
})
.Unwrap()
.ContinueWith(baconTask =>
{
var bacon = baconTask.Result;
Console.WriteLine("Bacon ready, starting toast...");
return StartToastingBreadAsync();
})
.Unwrap()
.ContinueWith(toastTask =>
{
var toast = toastTask.Result;
Console.WriteLine("Toast ready, applying butter...");
return ApplyButterAsync(toast);
})
.Unwrap()
.ContinueWith(butteredToastTask =>
{
var butteredToast = butteredToastTask.Result;
Console.WriteLine("Butter applied, applying jam...");
return ApplyJamAsync(butteredToast);
})
.Unwrap()
.ContinueWith(finalToastTask =>
{
var finalToast = finalToastTask.Result;
Console.WriteLine("Breakfast completed with ContinueWith!");
});
}
Uso de async/await
La misma secuencia de operaciones con async/await resulta mucho más natural.
// Using async/await - much cleaner and easier to read
static async Task MakeBreakfastWithAsyncAwait()
{
var eggs = await StartCookingEggsAsync();
Console.WriteLine("Eggs ready, starting bacon...");
var bacon = await StartCookingBaconAsync();
Console.WriteLine("Bacon ready, starting toast...");
var toast = await StartToastingBreadAsync();
Console.WriteLine("Toast ready, applying butter...");
var butteredToast = await ApplyButterAsync(toast);
Console.WriteLine("Butter applied, applying jam...");
var finalToast = await ApplyJamAsync(butteredToast);
Console.WriteLine("Breakfast completed with async/await!");
}
¿Por qué se prefiere async/await?
El async/await enfoque ofrece varias ventajas:
- Legibilidad: el código lee como código sincrónico, lo que facilita la comprensión del flujo de operaciones.
- Capacidad de mantenimiento: agregar o quitar pasos en la secuencia requiere cambios mínimos en el código.
-
Control de errores: el control de excepciones con
try/catchbloques funciona de forma natural, mientras queContinueWithrequiere un control cuidadoso de las tareas con errores. -
Depuración: La experiencia con la pila de llamadas y el depurador es mucho mejor con
async/await. -
Rendimiento: las optimizaciones del compilador para
async/awaitson más sofisticadas que las cadenas manuales.ContinueWith
La ventaja se vuelve aún más evidente a medida que aumenta el número de operaciones encadenadas. Aunque una sola continuación puede ser manejable con ContinueWith, las secuencias de 3 a 4 o más operaciones asincrónicas se vuelven rápidamente difíciles de leer y mantener. Este patrón, conocido como "notación monádica" en la programación funcional, permite componer varias operaciones asincrónicas de una manera secuencial y legible.