Async en profundidadAsync in depth

La escritura de código asincrónico enlazado a E/S y CPU es sencilla al usar el modelo asincrónico basado en tareas de .NET.Writing I/O- and CPU-bound asynchronous code is straightforward using the .NET Task-based async model. El modelo se expone mediante los tipos Task y Task<T> y las palabras claves async y await en C# y Visual Basic.The model is exposed by the Task and Task<T> types and the async and await keywords in C# and Visual Basic. (Los recursos específicos del idioma se encuentran en la sección Vea también). En este artículo, se explica cómo usar Async de .NET y se proporciona información sobre el marco de trabajo de Async usado en segundo plano.(Language-specific resources are found in the See also section.) This article explains how to use .NET async and provides insight into the async framework used under the covers.

Task y Task<T>Task and Task<T>

Las tareas son construcciones que se usan para implementar lo que se conoce como el modelo de promesa de simultaneidad.Tasks are constructs used to implement what is known as the Promise Model of Concurrency. En resumen, le ofrecen una "promesa" de que el trabajo se completará en un momento posterior, lo que le permite coordinarse con la promesa con una API limpia.In short, they offer you a "promise" that work will be completed at a later point, letting you coordinate with the promise with a clean API.

  • Task representa una única operación que no devuelve un valor.Task represents a single operation which does not return a value.
  • Task<T> representa una única operación que devuelve un valor de tipo T.Task<T> represents a single operation which returns a value of type T.

Es importante razonar sobre las tareas como abstracciones de trabajo que se producen de forma asincrónica y no una abstracción sobre subprocesos.It’s important to reason about tasks as abstractions of work happening asynchronously, and not an abstraction over threading. De manera predeterminada, las tareas se ejecutan en el trabajo de subproceso y delegado actual del sistema operativo, según corresponda.By default, tasks execute on the current thread and delegate work to the Operating System, as appropriate. De forma opcional, se puede solicitar de forma explícita que se ejecuten las tareas en un subproceso independiente mediante la API Task.Run.Optionally, tasks can be explicitly requested to run on a separate thread via the Task.Run API.

Las tareas exponen un protocolo de API para supervisar, esperar y acceder al valor del resultado (en el caso de Task<T>) de una tarea.Tasks expose an API protocol for monitoring, waiting upon and accessing the result value (in the case of Task<T>) of a task. La integración de lenguajes, con la palabra clave await, proporciona una abstracción de alto nivel para usar tareas.Language integration, with the await keyword, provides a higher-level abstraction for using tasks.

Mediante await, su aplicación o servicio puede realizar trabajo útil mientras se ejecuta una tarea al ceder el control a su llamador hasta que se realiza la tarea.Using await allows your application or service to perform useful work while a task is running by yielding control to its caller until the task is done. El código no tiene que depender de las devoluciones de llamada ni eventos para seguir ejecutándose una vez completada la tarea.Your code does not need to rely on callbacks or events to continue execution after the task has been completed. La integración de la API de tareas y lenguajes se encarga de ello.The language and task API integration does that for you. Si está usando Task<T>, la palabra clave await "desencapsulará" también el valor devuelto cuando se completa la tarea.If you’re using Task<T>, the await keyword will additionally "unwrap" the value returned when the Task is complete. Más adelante se explican los detalles sobre cómo funciona esto.The details of how this works are explained further below.

Puede obtener más información sobre las tareas y las distintas formas de interactuar con ellas en el tema Task-based Asynchronous Pattern (TAP) (Modelo asincrónico basado en tareas [TAP]).You can learn more about tasks and the different ways to interact with them in the Task-based Asynchronous Pattern (TAP) topic.

Tareas para una operación enlazada a E/S en profundidadDeeper Dive into Tasks for an I/O-Bound Operation

En la siguiente sección, se describe una vista general de lo que sucede con una llamada de E/S asincrónica normal.The following section describes a 10,000 foot view of what happens with a typical async I/O call. Comencemos con un par de ejemplos.Let's start with a couple examples.

En el primer ejemplo, se llama a un método asincrónico y se devuelve una tarea activa que, probablemente, aún esté sin completar.The first example calls an async method and returns an active task, likely yet to complete.

public Task<string> GetHtmlAsync()
{
    // Execution is synchronous here
    var client = new HttpClient();

    return client.GetStringAsync("https://www.dotnetfoundation.org");
}

En el segundo ejemplo, se agrega el uso de las palabras clave async y await para que funcionen en la tarea.The second example adds the use of the async and await keywords to operate on the task.

public async Task<string> GetFirstCharactersCountAsync(string url, int count)
{
    // Execution is synchronous here
    var client = new HttpClient();

    // Execution of GetFirstCharactersCountAsync() is yielded to the caller here
    // GetStringAsync returns a Task<string>, which is *awaited*
    var page = await client.GetStringAsync("https://www.dotnetfoundation.org");

    // Execution resumes when the client.GetStringAsync task completes,
    // becoming synchronous again.

    if (count > page.Length)
    {
        return page;
    }
    else
    {
        return page.Substring(0, count);
    }
}

La llamada a GetStringAsync() se realiza a través de bibliotecas de .NET de nivel inferior (es posible que se llame a otros métodos asincrónicos) hasta que se alcanza una llamada de interoperabilidad de P/Invoke en una biblioteca de red nativa.The call to GetStringAsync() calls through lower-level .NET libraries (perhaps calling other async methods) until it reaches a P/Invoke interop call into a native networking library. La biblioteca nativa puede realizar posteriormente una llamada API del sistema (como write() a un socket en Linux).The native library may subsequently call into a System API call (such as write() to a socket on Linux). Se creará un objeto de tarea en el límite nativo o administrado, posiblemente mediante TaskCompletionSource.A task object will be created at the native/managed boundary, possibly using TaskCompletionSource. El objeto de tarea se pasará por las capas, funcionará en ellas o se devolverá directamente y, finalmente, se devuelve al llamador inicial.The task object will be passed up through the layers, possibly operated on or directly returned, eventually returned to the initial caller.

En este segundo ejemplo, se devolverá un objeto Task<T> de GetStringAsync.In the second example above, a Task<T> object will be returned from GetStringAsync. El uso de la palabra clave await hace que el método devuelva un objeto de tarea recién creado.The use of the await keyword causes the method to return a newly created task object. El control vuelve al llamador de esta ubicación en el método GetFirstCharactersCountAsync.Control returns to the caller from this location in the GetFirstCharactersCountAsync method. Los métodos y propiedades del objeto Task<T> permiten que los llamadores supervisen el progreso de la tarea, que se completará cuando se haya ejecutado el código restante en GetFirstCharactersCountAsync.The methods and properties of the Task<T> object enable callers to monitor the progress of the task, which will complete when the remaining code in GetFirstCharactersCountAsync has executed.

Después de la llamada API del sistema, la solicitud ahora está en el espacio del kernel, que avanza hacia el subsistema de red del sistema operativo (como /net en Linux Kernel).After the System API call, the request is now in kernel space, making its way to the networking subsystem of the OS (such as /net in the Linux Kernel). Aquí, el sistema operativo controlará la solicitud de red de forma asincrónica.Here the OS will handle the networking request asynchronously. Los detalles pueden ser diferentes según el sistema operativo usado (la llamada al controlador de dispositivo puede programarse como una señal devuelta al tiempo de ejecución o una llamada al controlador de dispositivo puede realizarse y después se devuelve una señal), pero, finalmente, el tiempo de ejecución recibirá la información de que la solicitud de red está en curso.Details may be different depending on the OS used (the device driver call may be scheduled as a signal sent back to the runtime, or a device driver call may be made and then a signal sent back), but eventually the runtime will be informed that the networking request is in progress. En este momento, el trabajo del controlador de dispositivo estará programado, en curso o ya estará terminado (la solicitud ya estará "en la conexión"), pero como esto se produce de forma asincrónica, el controlador de dispositivo es capaz de controlar otra cosa de forma inmediata.At this time, the work for the device driver will either be scheduled, in-progress, or already finished (the request is already out "over the wire") - but because this is all happening asynchronously, the device driver is able to immediately handle something else!

Por ejemplo, en Windows, un subproceso de sistema operativo realiza una llamada al controlador de dispositivo de red y le pide que realice la operación de red a través de un paquete de petición de interrupción (IRP) que representa la operación.For example, in Windows an OS thread makes a call to the network device driver and asks it to perform the networking operation via an Interrupt Request Packet (IRP) which represents the operation. El controlador de dispositivo recibe el IRP, realiza la llamada a la red, marca el IRP como "pendiente" y vuelve al sistema operativo.The device driver receives the IRP, makes the call to the network, marks the IRP as "pending", and returns back to the OS. Ya que el subproceso de sistema operativo ahora sabe que el IRP está "pendiente", no tiene nada más que hacer en este trabajo y "vuelve", de modo que se puede usar para realizar otro trabajo.Because the OS thread now knows that the IRP is "pending", it doesn't have any more work to do for this job and "returns" back so that it can be used to perform other work.

Cuando se haya realizado la solicitud y regresen los datos a través del controlador de dispositivo, notifica a la CPU que se han recibido nuevos datos mediante una interrupción.When the request is fulfilled and data comes back through the device driver, it notifies the CPU of new data received via an interrupt. La forma en que se controla esta interrupción varía según el sistema operativo, pero, finalmente, los datos pasarán a través del sistema operativo hasta que lleguen a una llamada de interoperabilidad del sistema (por ejemplo, en Linux un controlador de interrupciones programará la mitad inferior de la IRQ para pasar los datos a través del sistema operativo de forma asincrónica).How this interrupt gets handled will vary depending on the OS, but eventually the data will be passed through the OS until it reaches a system interop call (for example, in Linux an interrupt handler will schedule the bottom half of the IRQ to pass the data up through the OS asynchronously). Tenga en cuenta que esto también se produce de manera asincrónica.Note that this also happens asynchronously! El resultado se pone en cola hasta que el siguiente subproceso disponible puede ejecutar el método asincrónico y "desencapsular" el resultado de la tarea completada.The result is queued up until the next available thread is able to execute the async method and "unwrap" the result of the completed task.

A lo largo de todo este proceso, un punto clave es que ningún subproceso se dedica a ejecutar la tarea.Throughout this entire process, a key takeaway is that no thread is dedicated to running the task. Aunque el trabajo se ejecuta en algún contexto (es decir, el sistema operativo tiene que pasar datos a un controlador de dispositivo y responder a una interrupción), no hay ningún subproceso dedicado a esperar a que vuelvan los datos de la solicitud.Although work is executed in some context (that is, the OS does have to pass data to a device driver and respond to an interrupt), there is no thread dedicated to waiting for data from the request to come back. Esto permite al sistema controlar un volumen de trabajo mucho mayor en lugar de esperar a que finalicen algunas llamadas de E/S.This allows the system to handle a much larger volume of work rather than waiting for some I/O call to finish.

Aunque lo anterior puede parecer mucho trabajo que realizar, al medirlo en términos de tiempo de reloj, es ínfimo en comparación con el tiempo necesario para realizar el trabajo de E/S real.Although the above may seem like a lot of work to be done, when measured in terms of wall clock time, it’s miniscule compared to the time it takes to do the actual I/O work. Aunque no es exacta, una escala de tiempo posible para una llamada de este estilo tendría este aspecto:Although not at all precise, a potential timeline for such a call would look like this:

0-1————————————————————————————————————————————————–2-30-1————————————————————————————————————————————————–2-3

  • El tiempo empleado de los puntos 0 a 1 es todo hasta que un método asincrónico cede el control a su llamador.Time spent from points 0 to 1 is everything up until an async method yields control to its caller.
  • El tiempo empleado de los puntos 1 a 2 es el tiempo transcurrido en E/S, sin ningún costo de CPU.Time spent from points 1 to 2 is the time spent on I/O, with no CPU cost.
  • Por último, el tiempo empleado de los puntos 2 a 3 es durante el que se pasa el control (y posiblemente un valor) de nuevo al método asincrónico, momento en que se vuelve a ejecutar.Finally, time spent from points 2 to 3 is passing control back (and potentially a value) to the async method, at which point it is executing again.

¿Qué significa esto en un escenario de servidor?What does this mean for a server scenario?

Este modelo funciona bien con una carga de trabajo de escenario de servidor típica.This model works well with a typical server scenario workload. Ya que no hay ningún subproceso dedicado al bloqueo de tareas incompletas, el grupo de subprocesos de servidor puede atender a un mayor volumen de solicitudes web.Because there are no threads dedicated to blocking on unfinished tasks, the server threadpool can service a much higher volume of web requests.

Considere dos servidores: uno que ejecute código asincrónico y otro que no lo haga.Consider two servers: one that runs async code, and one that does not. Para este ejemplo, cada servidor tiene solo 5 subprocesos disponibles para las solicitudes de servicio.For the purpose of this example, each server only has 5 threads available to service requests. Tenga en cuenta que estos números son pequeños y sirven solo en un contexto demostrativo.Note that these numbers are imaginarily small and serve only in a demonstrative context.

Suponga que ambos servidores reciben seis solicitudes simultáneas.Assume both servers receive 6 concurrent requests. Cada solicitud realiza una operación de E/S.Each request performs an I/O operation. El servidor sin código asincrónico tiene que poner en cola la solicitud 6 hasta que uno de los 5 subprocesos haya finalizado el trabajo enlazado a E/S y escrito una respuesta.The server without async code has to queue up the 6th request until one of the 5 threads have finished the I/O-bound work and written a response. En el momento en que entre la solicitud 20, el servidor puede comenzar a ralentizarse, porque la cola se extiende demasiado.At the point that the 20th request comes in, the server might start to slow down, because the queue is getting too long.

El servidor con código asincrónico en ejecución también pone en cola la solicitud 6, pero ya que usa async y await, cada uno de los subprocesos se libera cuando se inicia el trabajo enlazado a E/S, en lugar de cuando finaliza.The server with async code running on it still queues up the 6th request, but because it uses async and await, each of its threads are freed up when the I/O-bound work starts, rather than when it finishes. Cuando llega la solicitud 20, la cola de solicitudes entrantes será mucho más pequeña (en caso de que haya algo) y no se ralentiza el servidor.By the time the 20th request comes in, the queue for incoming requests will be far smaller (if it has anything in it at all), and the server won't slow down.

Aunque se trata de un ejemplo inventado, ocurre de forma muy similar en el mundo real.Although this is a contrived example, it works in a very similar fashion in the real world. De hecho, puede esperar que un servidor pueda controlar más solicitudes mediante async y await que si se dedicaba a un subproceso para cada solicitud que recibe.In fact, you can expect a server to be able to handle an order of magnitude more requests using async and await than if it were dedicating a thread for each request it receives.

¿Qué significa esto en un escenario de cliente?What does this mean for client scenario?

El mayor beneficio al usar async y await para una aplicación cliente es un aumento en la capacidad de respuesta.The biggest gain for using async and await for a client app is an increase in responsiveness. Aunque puede crear una aplicación dinámica al generar subprocesos de forma manual, el hecho de generar un subproceso es una operación costosa en comparación a usar solo async y await.Although you can make an app responsive by spawning threads manually, the act of spawning a thread is an expensive operation relative to just using async and await. Especialmente en el caso de juegos para móviles, es fundamental afectar lo mínimo posible al subproceso de interfaz de usuario en lo que a E/S se refiere.Especially for something like a mobile game, impacting the UI thread as little as possible where I/O is concerned is crucial.

Sobre todo, ya que el trabajo enlazado a E/S no invierte prácticamente ningún tiempo en la CPU, dedicar un subproceso de CPU completo a realizar prácticamente ningún trabajo útil sería un mal uso de recursos.More importantly, because I/O-bound work spends virtually no time on the CPU, dedicating an entire CPU thread to perform barely any useful work would be a poor use of resources.

Además, es muy sencillo enviar trabajo al subproceso de interfaz de usuario (como actualizar una interfaz de usuario) con métodos async y no requiere trabajo adicional (como llamar a un delegado seguro para subprocesos).Additionally, dispatching work to the UI thread (such as updating a UI) is very simple with async methods, and does not require extra work (such as calling a thread-safe delegate).

Task y Task<T> para una operación enlazada a la CPU en profundidadDeeper Dive into Task and Task<T> for a CPU-Bound Operation

El código async enlazado a la CPU es un poco diferente del código async enlazado a E/S.CPU-bound async code is a bit different than I/O-bound async code. Ya que el trabajo se realiza en la CPU, no hay ninguna forma de evitar dedicar un subproceso al cálculo.Because the work is done on the CPU, there's no way to get around dedicating a thread to the computation. El uso de async y await le proporciona una manera clara de interactuar con subprocesos en segundo plano y mantener al llamador del método asincrónico dinámico.The use of async and await provides you with a clean way to interact with a background thread and keep the caller of the async method responsive. Tenga en cuenta que esto no proporciona ninguna protección para datos compartidos.Note that this does not provide any protection for shared data. Si usa datos compartidos, aún tendrá que aplicar una estrategia de sincronización adecuada.If you are using shared data, you will still need to apply an appropriate synchronization strategy.

Esta es una vista general de una llamada asincrónica enlazada a la CPU:Here's a 10,000 foot view of a CPU-bound async call:

public async Task<int> CalculateResult(InputData data)
{
    // This queues up the work on the threadpool.
    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));

    // Note that at this point, you can do some other work concurrently,
    // as CalculateResult() is still executing!

    // Execution of CalculateResult is yielded here!
    var result = await expensiveResultTask;

    return result;
}

CalculateResult() se ejecuta en el subproceso en que se ha llamado.CalculateResult() executes on the thread it was called on. Cuando llama a Task.Run, pone en cola la operación costosa enlazada a la CPU, DoExpensiveCalculation(), en el grupo de subprocesos y recibe un controlador Task<int>.When it calls Task.Run, it queues the expensive CPU-bound operation, DoExpensiveCalculation(), on the thread pool and receives a Task<int> handle. DoExpensiveCalculation() se ejecuta finalmente de forma simultánea en el siguiente subproceso disponible, probablemente en otro núcleo de CPU.DoExpensiveCalculation() is eventually run concurrently on the next available thread, likely on another CPU core. Es posible realizar trabajo simultáneo mientras DoExpensiveCalculation() está ocupado en otro subproceso, ya que el subproceso que llama a CalculateResult() aún se está ejecutando.It's possible to do concurrent work while DoExpensiveCalculation() is busy on another thread, because the thread which called CalculateResult() is still executing.

Una vez se encuentra await, la ejecución de CalculateResult() se cede a su llamador, lo que permite que se realice otro trabajo con el subproceso actual mientras DoExpensiveCalculation() genera un resultado.Once await is encountered, the execution of CalculateResult() is yielded to its caller, allowing other work to be done with the current thread while DoExpensiveCalculation() is churning out a result. Una vez que haya finalizado, el resultado se pone en la cola para ejecutarse en el subproceso principal.Once it has finished, the result is queued up to run on the main thread. Finalmente, el subproceso principal volverá a ejecutar CalculateResult(), momento en que tendrá el resultado de DoExpensiveCalculation().Eventually, the main thread will return to executing CalculateResult(), at which point it will have the result of DoExpensiveCalculation().

¿Por qué ayuda Async en este caso?Why does async help here?

async y await son el procedimiento recomendado para administrar el trabajo enlazado a la CPU si necesita capacidad de respuesta.async and await are the best practice for managing CPU-bound work when you need responsiveness. Hay varios patrones para usar Async con trabajo enlazado a la CPU.There are multiple patterns for using async with CPU-bound work. Es importante tener en cuenta que hay un pequeño costo al usar Async y no se recomienda para bucles de pequeñas dimensiones.It's important to note that there is a small cost to using async and it's not recommended for tight loops. Depende de usted determinar cómo escribe el código en torno a esta nueva funcionalidad.It's up to you to determine how you write your code around this new capability.

Vea tambiénSee also