Modelo de programación asincrónica de tareas en C#The Task asynchronous programming model in C#

El modelo de programación asincrónica de tareas (TAP) es una abstracción del código asincrónico.The Task asynchronous programming model (TAP) provides an abstraction over asynchronous code. El código se escribe como una secuencia de instrucciones, como es habitual.You write code as a sequence of statements, just like always. Puede leerlo como si cada instrucción se completase antes de comenzar la siguiente.You can read that code as though each statement completes before the next begins. El compilador realiza una serie de transformaciones, debido a que algunas de estas instrucciones podrían empezar a funcionar y devolver una clase Task que representa el trabajo en curso.The compiler performs a number of transformations because some of those statements may start work and return a Task that represents the ongoing work.

Este es el objetivo de la sintaxis: habilitar código que se lea como una secuencia de instrucciones, pero que se ejecute siguiendo un orden mucho más complicado, en función de la asignación de recursos externos y del momento en el que se completen las tareas.That's the goal of this syntax: enable code that reads like a sequence of statements, but executes in a much more complicated order based on external resource allocation and when tasks complete. Es similar a la manera en la que las personas dan instrucciones para los procesos que incluyen las tareas asincrónicas.It's analogous to how people give instructions for processes that include asynchronous tasks. En este artículo, usará un ejemplo con instrucciones para preparar el desayuno que le ayudará a comprender cómo las palabras clave async y await facilitan el proceso de razonar sobre el código que incluye una serie de instrucciones asincrónicas.Throughout this article, you'll use an example of instructions for making a breakfast to see how the async and await keywords make it easier to reason about code that includes a series of asynchronous instructions. Para explicar cómo se prepara un desayuno, probablemente escribirá unas instrucciones parecidas a las que se recogen en la lista siguiente:You'd write the instructions something like the following list to explain how to make a breakfast:

  1. Sirva una taza de café.Pour a cup of coffee.
  2. Caliente una sartén y fría dos huevos.Heat up a pan, then fry two eggs.
  3. Fría tres lonchas de beicon.Fry three slices of bacon.
  4. Tueste dos rebanadas de pan.Toast two pieces of bread.
  5. Unte el pan con mantequilla y mermelada.Add butter and jam to the toast.
  6. Sirva un vaso de zumo de naranja.Pour a glass of orange juice.

Si tiene experiencia en la cocina, lo más probable es que ejecute estas instrucciones de forma asincrónica.If you have experience with cooking, you'd execute those instructions asynchronously. Primero, calentará la sartén para los huevos e irá friendo el beicon.You'd start warming the pan for eggs, then start the bacon. Después, pondrá el pan en la tostadora y empezará a freír los huevos.You'd put the bread in the toaster, then start the eggs. En cada paso del proceso, iniciará una tarea y volverá la atención a las tareas que tiene pendientes.At each step of the process, you'd start a task, then turn your attention to tasks that are ready for your attention.

La preparación del desayuno es un buen ejemplo de un trabajo asincrónico que no es paralelo.Cooking breakfast is a good example of asynchronous work that isn't parallel. Una persona (o un subproceso) puede controlar todas estas tareas.One person (or thread) can handle all these tasks. Siguiendo con la analogía del desayuno, una persona puede preparar el desayuno asincrónicamente si comienza la tarea siguiente antes de que finalice la anterior.Continuing the breakfast analogy, one person can make breakfast asynchronously by starting the next task before the first completes. Los alimentos se cocinan tanto si una persona supervisa el proceso como si no.The cooking progresses whether or not someone is watching it. En cuanto se empieza a calentar la sartén para los huevos, se puede comenzar a freír el beicon.As soon as you start warming the pan for the eggs, you can begin frying the bacon. Una vez que el beicon se esté haciendo, se puede poner el pan en la tostadora.Once the bacon starts, you can put the bread into the toaster.

En el caso de un algoritmo paralelo, necesitaría varios cocineros (o subprocesos).For a parallel algorithm, you'd need multiple cooks (or threads). Uno se encargaría de los huevos, otro del beicon, etc.One would make the eggs, one the bacon, and so on. Cada uno de ellos se centraría en una sola tarea.Each one would be focused on just that one task. Un cocinero (o subproceso) se bloqueará al esperar asincrónicamente a que el beicon se dore para darle la vuelta, o al esperar a que las tostadas estén listas.Each cook (or thread) would be blocked synchronously waiting for bacon to be ready to flip, or the toast to pop.

Piense ahora en estas mismas instrucciones escritas como instrucciones de C#:Now, consider those same instructions written as C# statements:

static void Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = FryBacon(3);
    Console.WriteLine("bacon is 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!");
}

Los equipos no interpretan estas instrucciones de la misma manera que las personas.Computers don't interpret those instructions the same way people do. El equipo se bloqueará en cada instrucción hasta que el trabajo se complete antes de pasar a la instrucción siguiente.The computer will block on each statement until the work is complete before moving on to the next statement. Podría decirse que esto da lugar a un desayuno poco satisfactorio.That creates an unsatisfying breakfast. Las tareas posteriores no se pueden iniciar mientras no se completen las anteriores.The later tasks wouldn't be started until the earlier tasks had completed. Así pues, se tardará mucho más en preparar el desayuno y algunos alimentos se habrán enfriado incluso antes de servirse.It would take much longer to create the breakfast, and some items would have gotten cold before being served.

Si quiere que el equipo ejecute las instrucciones anteriores de forma asincrónica, debe escribir código asincrónico.If you want the computer to execute the above instructions asynchronously, you must write asynchronous code.

Estas cuestiones son importantes para los programas que se escriben hoy en día.These concerns are important for the programs you write today. Al escribir programas cliente, le interesa que la interfaz de usuario responda a la entrada del usuario.When you write client programs, you want the UI to be responsive to user input. La aplicación no debería hacer que un teléfono parezca congelado mientras descarga datos de la Web.Your application shouldn't make a phone appear frozen while it's downloading data from the web. Al escribir programas de servidor, no le conviene que los subprocesos se bloqueen.When you write server programs, you don't want threads blocked. La intención es que puedan atender también otras solicitudes.Those threads could be serving other requests. El uso de código sincrónico cuando existen alternativas asincrónicas va en detrimento de la capacidad de escalar horizontalmente a un menor coste.Using synchronous code when asynchronous alternatives exist hurts your ability to scale out less expensively. Al final, los subprocesos bloqueados pasarán factura.You pay for those blocked threads.

Las aplicaciones modernas de más éxito requieren código asincrónico.Successful modern applications require asynchronous code. Sin la compatibilidad con los lenguajes, la escritura de código asincrónico requería devoluciones de llamada, eventos de finalización u otros medios que impedían ver claramente la intención original del código.Without language support, writing asynchronous code required callbacks, completion events, or other means that obscured the original intent of the code. La ventaja del código sincrónico es que resulta fácil de entender.The advantage of the synchronous code is that it's easy to understand. Las acciones detalladas hacen que sea fácil examinar el código y comprenderlo.The step-by-step actions make it easy to scan and understand. Los modelos asincrónicos tradicionales obligaban a centrarse en la naturaleza asincrónica del código, no en las acciones fundamentales.Traditional asynchronous models forced you to focus on the asynchronous nature of the code, not on the fundamental actions of the code.

Uso de await para evitar los bloqueosDon't block, await instead

El código anterior muestra una práctica incorrecta, que consiste en construir código sincrónico para llevar a cabo operaciones asincrónicas.The preceding code demonstrates a bad practice: constructing synchronous code to perform asynchronous operations. Tal como está escrito, este código bloquea el subproceso que lo ejecuta y le impide realizar cualquier otro trabajo.As written, this code blocks the thread executing it from doing any other work. Nada lo interrumpirá mientras alguna de las tareas esté en curso.It won't be interrupted while any of the tasks are in progress. Es como si usted se quedara mirando la tostadora después de meter el panIt would be as though you stared at the toaster after putting the bread in. y no pudiera oír a nadie que le dirigiera la palabra hasta que las tostadas estuvieran listas.You'd ignore anyone talking to you until the toast popped.

Empecemos por actualizar este código para que el subproceso no se bloquee mientras se ejecutan las tareas.Let's start by updating this code so that the thread doesn't block while tasks are running. La palabra clave await proporciona un modo sin bloqueo para iniciar una tarea y, después, proseguir la ejecución cuando dicha tarea se complete.The await keyword provides a non-blocking way to start a task, then continue execution when that task completes. Una versión asincrónica sencilla del código para preparar el desayuno tendría un aspecto parecido al del fragmento siguiente:A simple asynchronous version of the make a breakfast code would look like the following snippet:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = await FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = await FryBacon(3);
    Console.WriteLine("bacon is ready");
    Toast toast = await ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");
}

Este código no produce un bloqueo mientras se cocinan los huevos o el beicon,This code doesn't block while the eggs or the bacon are cooking. pero tampoco inicia otras tareas.This code won't start any other tasks though. Es decir, pondría el pan en la tostadora y se quedaría esperando a que estuviera listo,You'd still put the toast in the toaster and stare at it until it pops. pero, por lo menos, si alguien reclamara su atención, le haría caso.But at least, you'd respond to anyone that wanted your attention. En un restaurante en el que se atienden varios pedidos, el cocinero empezaría a preparar otro desayuno mientras se hace el primero.In a restaurant where multiple orders are placed, the cook could start another breakfast while the first is cooking.

El subproceso que se encarga del desayuno ya no se bloquearía mientras espera por las tareas iniciadas que aún no han terminado.Now, the thread working on the breakfast isn't blocked while awaiting any started task that hasn't yet finished. En algunas aplicaciones, lo único que se necesita es este cambio.For some applications, this change is all that's needed. Una aplicación de interfaz gráfica de usuario seguirá respondiendo al usuario con este único cambio.A GUI application still responds to the user with just this change. Aun así, para este escenario, necesita algo más.However, for this scenario, you want more. No le interesa que todas las tareas de componente se ejecuten secuencialmente.You don't want each of the component tasks to be executed sequentially. Es mejor iniciar cada una de estas tareas sin esperar a que la tarea anterior se complete.It's better to start each of the component tasks before awaiting the previous task's completion.

Inicio simultáneo de tareasStart tasks concurrently

En muchos escenarios, necesita iniciar varias tareas independientes de inmediato.In many scenarios, you want to start several independent tasks immediately. Después, a medida que finalice cada tarea, podrá seguir con el trabajo que esté listo.Then, as each task finishes, you can continue other work that's ready. Siguiendo con la analogía del desayuno, esta es la manera más rápida de prepararlo.In the breakfast analogy, that's how you get breakfast done more quickly. Además, de este modo, todo estará listo aproximadamente en el mismo momento.You also get everything done close to the same time. Así podrá disfrutar de un desayuno caliente.You'll get a hot breakfast.

La clase System.Threading.Tasks.Task y los tipos relacionados se pueden usar para razonar sobre las tareas que están en curso.The System.Threading.Tasks.Task and related types are classes you can use to reason about tasks that are in progress. Esto le permite escribir código que se asemeje más a la manera en que realmente se prepara el desayuno.That enables you to write code that more closely resembles the way you'd actually create breakfast. Para ello, cocinará los huevos, el beicon y las tostadas al mismo tiempo.You'd start cooking the eggs, bacon, and toast at the same time. Como cada uno de estos elementos requiere una acción, se ocupará de esa tarea, se encargará de la acción siguiente y esperará por todo lo que necesite su atención.As each requires action, you'd turn your attention to that task, take care of the next action, then await for something else that requires your attention.

Comenzará una tarea y conservará el objeto Task que representa el trabajo.You start a task and hold on to the Task object that represents the work. Llevará a cabo una instrucción await para esperar por cada tarea antes de trabajar con su resultado.You'll await each task before working with its result.

Realicemos estos cambios en el código del desayuno.Let's make these changes to the breakfast code. El primer paso consiste en almacenar las tareas de las operaciones cuando se inician, en lugar de esperar por ellas:The first step is to store the tasks for operations when they start, rather than awaiting them:

Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggTask = FryEggs(2);
Egg eggs = await eggTask;
Console.WriteLine("eggs are ready");
Task<Bacon> baconTask = FryBacon(3);
Bacon bacon = await baconTask;
Console.WriteLine("bacon is ready");
Task<Toast> toastTask = ToastBread(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!");

Después, puede mover las instrucciones await del beicon y los huevos al final del método, antes de servir el desayuno:Next, you can move the await statements for the bacon and eggs to the end of the method, before serving breakfast:

Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggTask = FryEggs(2);
Task<Bacon> baconTask = FryBacon(3);
Task<Toast> toastTask = ToastBread(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 eggTask;
Console.WriteLine("eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("bacon is ready");

Console.WriteLine("Breakfast is ready!");

El código anterior funciona mejor.The preceding code works better. Iniciará todas las tareas asincrónicas a la vezYou start all the asynchronous tasks at once. y esperará por una tarea solo cuando necesite los resultados.You await each task only when you need the results. El código anterior se parece al código de una aplicación web que realiza solicitudes a diferentes microservicios y, después, combina los resultados en una sola página.The preceding code may be similar to code in a web application that makes requests of different microservices, then combines the results into a single page. Podrá realizar todas las solicitudes de inmediato y, luego, llevará a cabo una instrucción await para esperar por todas esas tareas y componer la página web.You'll make all the requests immediately, then await all those tasks and compose the web page.

Composición con tareasComposition with tasks

Ya tiene todo listo al mismo tiempo para el desayuno, excepto las tostadas.You have everything ready for breakfast at the same time except the toast. La preparación de las tostadas es la composición de una operación asincrónica (tostar el pan) y varias operaciones sincrónicas (untar la mantequilla y la mermelada).Making the toast is the composition of an asynchronous operation (toasting the bread), and synchronous operations (adding the butter and the jam). La actualización de este código ilustra un concepto importante:Updating this code illustrates an important concept:

Importante

La composición de una operación asincrónica seguida de un trabajo sincrónico es una operación asincrónica.The composition of an asynchronous operation followed by synchronous work is an asynchronous operation. Dicho de otra manera, si una parte cualquiera de una operación es asincrónica, toda la operación es asincrónica.Stated another way, if any portion of an operation is asynchronous, the entire operation is asynchronous.

En el código anterior se muestra que se puede usar un objeto Task o Task<TResult> para conservar tareas en ejecución.The preceding code showed you that you can use Task or Task<TResult> objects to hold running tasks. Lleva a cabo una instrucción await para esperar por una tarea a fin de poder usar su resultado.You await each task before using its result. El siguiente paso consiste en crear métodos que representan la combinación de otro trabajo.The next step is to create methods that represent the combination of other work. Antes de servir el desayuno, quiere esperar por la tarea que representa tostar el pan antes de untar la mantequilla y la mermelada.Before serving breakfast, you want to await the task that represents toasting the bread before adding butter and jam. Puede representar este trabajo con el código siguiente:You can represent that work with the following code:

async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    var toast = await ToastBreadAsync(number);
    ApplyButter(toast);
    ApplyJam(toast);
    return toast;
}

El método anterior tiene el modificador async en su firma,The preceding method has the async modifier in its signature. lo que indica al compilador que este método incluye una instrucción await, es decir, que contiene operaciones asincrónicas.That signals to the compiler that this method contains an await statement; it contains asynchronous operations. Este método representa la tarea que tuesta el pan y, después, agrega la mantequilla y la mermelada.This method represents the task that toasts the bread, then adds butter and jam. El método devuelve un objeto Task<TResult> que representa la composición de estas tres operaciones.This method returns a Task<TResult> that represents the composition of those three operations. El bloque principal de código se convierte en lo siguiente:The main block of code now becomes:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    var eggsTask = FryEggsAsync(2);
    var baconTask = FryBaconAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);

    var eggs = await eggsTask;
    Console.WriteLine("eggs are ready");
    var bacon = await baconTask;
    Console.WriteLine("bacon is ready");
    var toast = await toastTask;
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");

    async Task<Toast> MakeToastWithButterAndJamAsync(int number)
    {
        var toast = await ToastBreadAsync(number);
        ApplyButter(toast);
        ApplyJam(toast);
        return toast;
    }
}

El cambio anterior ilustra una técnica importante para trabajar con código asincrónico.The previous change illustrated an important technique for working with asynchronous code. Para componer tareas, las operaciones se separan en un método nuevo que devuelve una tarea.You compose tasks by separating the operations into a new method that returns a task. Usted puede elegir cuándo se debe esperar por esta tareaYou can choose when to await that task. y puede iniciar otras tareas simultáneamente.You can start other tasks concurrently.

Espera de la finalización de las tareas de forma eficazAwait tasks efficiently

La serie de instrucciones await al final del código anterior se puede mejorar mediante el uso de métodos de la clase Task.The series of await statements at the end of the preceding code can be improved by using methods of the Task class. Una de estas API es WhenAll, que devuelve un objeto Task que se completa cuando finalizan todas las tareas de la lista de argumentos, como se muestra en el código siguiente:One of those APIs is WhenAll, which returns a Task that completes when all the tasks in its argument list have completed, as shown in the following code:

await Task.WhenAll(eggTask, baconTask, toastTask);
Console.WriteLine("eggs are ready");
Console.WriteLine("bacon is ready");
Console.WriteLine("toast is ready");
Console.WriteLine("Breakfast is ready!");

Otra opción consiste en usar WhenAny, que devuelve un objeto Task<Task> que se completa cuando finaliza cualquiera de sus argumentos.Another option is to use WhenAny, which returns a Task<Task> that completes when any of its arguments completes. Puede esperar por la tarea devuelta, con la certeza de saber que ya ha terminado.You can await the returned task, knowing that it has already finished. En el código siguiente se muestra cómo se puede usar WhenAny para esperar a que la primera tarea finalice y, después, procesar su resultado.The following code shows how you could use WhenAny to await the first task to finish and then process its result. Después de procesar el resultado de la tarea completada, quítela de la lista de tareas que se pasan a WhenAny.After processing the result from the completed task, you remove that completed task from the list of tasks passed to WhenAny.

var allTasks = new List<Task>{eggsTask, baconTask, toastTask};
while (allTasks.Any())
{
    Task finished = await Task.WhenAny(allTasks);
    if (finished == eggsTask)
    {
        Console.WriteLine("eggs are ready");
    }
    else if (finished == baconTask)
    {
        Console.WriteLine("bacon is ready");
    }
    else if (finished == toastTask)
    {
        Console.WriteLine("toast is ready");
    }
    allTasks.Remove(finished);
}
Console.WriteLine("Breakfast is ready!");

Después de todos estos cambios, la versión final de Main tiene un aspecto similar al código siguiente:After all those changes, the final version of Main looks like the following code:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    var eggsTask = FryEggsAsync(2);
    var baconTask = FryBaconAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);

    var allTasks = new List<Task>{eggsTask, baconTask, toastTask};
    while (allTasks.Any())
    {
        Task finished = await Task.WhenAny(allTasks);
        if (finished == eggsTask)
        {
            Console.WriteLine("eggs are ready");
        }
        else if (finished == baconTask)
        {
            Console.WriteLine("bacon is ready");
        }
        else if (finished == toastTask)
        {
            Console.WriteLine("toast is ready");
        }
        allTasks.Remove(finished);
    }
    Console.WriteLine("Breakfast is ready!");

    async Task<Toast> MakeToastWithButterAndJamAsync(int number)
    {
        var toast = await ToastBreadAsync(number);
        ApplyButter(toast);
        ApplyJam(toast);
        return toast;
    }
}

Este código final es asincrónico.This final code is asynchronous. Refleja con más precisión la manera en que una persona prepara un desayuno.It more accurately reflects how a person would cook a breakfast. Compare el código anterior con el primer ejemplo de código del artículo.Compare the preceding code with the first code sample in this article. Las acciones principales siguen siendo claras cuando se lee el código.The core actions are still clear from reading the code. De hecho, puede leerlo como si se tratara de las instrucciones para preparar el desayuno que se indican al principio del artículo.You can read this code the same way you'd read those instructions for making a breakfast at the beginning of this article. Las características del lenguaje para async y await proporcionan la traducción que cualquier persona haría para seguir las instrucciones escritas, a saber: las tareas deben iniciarse a medida que sea posible y debe evitarse el bloqueo por esperar a que se completen las tareas.The language features for async and await provide the translation every person makes to follow those written instructions: start tasks as you can and don't block waiting for tasks to complete.