Patrón asincrónico basado en tareas (TAP) en .NET: Introducción e información general

En .NET, el patrón asincrónico basado en tareas es el patrón de diseño asincrónico recomendado para el nuevo desarrollo. Se basa en los tipos Task y Task<TResult> del espacio de nombres System.Threading.Tasks, que se usan para representar operaciones asincrónicas.

Nombres, parámetros y tipos de valores devueltos

TAP usa un solo método para representar el inicio y la finalización de una operación asincrónica. Esto contrasta con el modelo de programación asincrónico (APM o IAsyncResult) y con el modelo asincrónico basado en eventos (EAP). APM requiere los métodos Begin y End. EPA requiere un método con el sufijo Async y también requiere uno o más eventos, tipos de delegado de controlador de eventos y tipos derivados de EventArg. Los métodos asincrónicos en TAP incluyen el sufijo Async después del nombre de la operación para los métodos que devuelven tipos que admiten await, como Task, Task<TResult>, ValueTask y ValueTask<TResult>. Por ejemplo, una operación Get asincrónica que devuelve un Task<String> se puede denominar GetAsync. Si va a agregar un método de TAP a una clase que ya contiene un nombre de método EAP con el sufijo Async, use el sufijo TaskAsync en su lugar. Por ejemplo, si la clase ya tiene un método GetAsync, use el nombre GetTaskAsync. Si un método inicia una operación asincrónica pero no devuelve un tipo que admite await, su nombre debe comenzar por Begin, Start u otro verbo que sugiera que este método no devuelve ni genera el resultado de la operación.  

Un método de TAP devuelve System.Threading.Tasks.Task o System.Threading.Tasks.Task<TResult>, en función de si el método sincrónico correspondiente devuelve void o un tipo TResult.

Los parámetros de un método de TAP deben coincidir con los parámetros de su homólogo sincrónico y se deben proporcionar en el mismo orden. Sin embargo, los parámetros out y ref están exentos de esta regla y se deben evitar completamente. En su lugar, los datos que se hubieran devuelto con un parámetro out o ref se deben devolver como parte del tipo TResult devuelto por Task<TResult> y deben usar una tupla o una estructura de datos personalizada para incluir varios valores. También debería plantearse la posibilidad de agregar un parámetro CancellationToken aunque el homólogo sincrónico del método TAP no ofrezca ninguno.

Los métodos que están dedicados exclusivamente a la creación, manipulación o combinación de tareas (donde el intento asincrónico del método está claro en el nombre del método o en el nombre del tipo al que el método pertenece) no necesitan seguir este modelo de nomenclatura; esos métodos se conocen a menudo como combinadores (también denominados elementos de combinación). Los ejemplos de combinadores incluyen WhenAll y WhenAny, y se describen en la sección que describe los combinadores integrados basados en tareas titulada Usar los combinadores integrados basados en tareas del artículo Utilizar el modelo asincrónico basado en tareas.

Para obtener ejemplos de cómo la sintaxis de TAP difiere de la sintaxis empleada en patrones heredados de programación asincrónica como el modelo de programación asincrónica (APM) y el patrón asincrónico basado en eventos (EAP), vea Modelos para la programación asincrónica.

Inicio de una operación asincrónica

Un método asincrónico basado en TAP puede hacer una pequeña cantidad de trabajo sincrónicamente, como validar argumentos e iniciar la operación asincrónica, antes de que devuelva la tarea resultante. El trabajo sincrónico debe reducirse al mínimo de modo que el método asincrónico pueda volver rápidamente. Entre las razones para un retorno rápido se incluyen las siguientes:

  • Los métodos asincrónicos se pueden invocar desde subprocesos de la interfaz de usuario (UI) y cualquier trabajo sincrónico de ejecución prolongada puede dañar la capacidad de respuesta de la aplicación.

  • Se pueden iniciar varios métodos asincrónicos simultáneamente. Por tanto, cualquier trabajo de ejecución prolongada en la parte sincrónica de un método asincrónico puede retrasar el inicio de otras operaciones asincrónicas, lo que reduce las ventajas de la simultaneidad.

En algunos casos, la cantidad de trabajo necesario para completar la operación es menor que la cantidad de trabajo necesario para iniciar la operación de forma asincrónica. La lectura de una secuencia donde la operación de lectura se puede satisfacer mediante datos que ya están almacenados en búfer en la memoria es un ejemplo de este escenario. En casos como este, la operación puede completarse sincrónicamente y puede devolver una tarea que ya se ha completado.

Excepciones

Un método asincrónico debe generar una excepción fuera de la llamada de método asincrónico solo como respuesta a un error de uso. Los errores de uso nunca deben producirse en código de producción. Por ejemplo, si al pasar una referencia nula (Nothing en Visual Basic) como uno de los argumentos del método se produce un estado de error (representado normalmente por una excepción ArgumentNullException), puede modificar el código de llamada para asegurarse de que nunca se pase una referencia nula. Para todos los demás errores, las excepciones que se producen cuando se ejecuta un método asincrónico deben asignarse a la tarea devuelta, aunque el método asincrónico se complete sincrónicamente antes de que se devuelva la tarea. Normalmente, una tarea contiene como máximo una excepción. Sin embargo, si la tarea representa varias operaciones (por ejemplo, WhenAll), se pueden asociar varias excepciones a una única tarea.

Entorno de destino

Cuando implementa un método de TAP, puede determinar dónde se produce la ejecución asincrónica. Puede optar por ejecutar la carga de trabajo en el grupo de subprocesos, implementarla mediante E/S asincrónica (sin enlazarse a un subproceso durante la mayor parte de la ejecución de la operación), ejecutarla en un subproceso concreto (como el subproceso de la interfaz de usuario) o usar cualquier número de contextos posibles. Un método de TAP incluso puede no tener nada que ejecutar y puede devolver simplemente una clase Task que representa la ocurrencia de una condición en otra ubicación en el sistema (por ejemplo, una tarea que representa datos que llegan a una estructura de datos en cola).

El autor de la llamada al método TAP puede bloquear la espera para que el método de TAP se complete mediante la espera sincrónica en la tarea resultante, o bien puede ejecutar código (de continuación) adicional cuando la operación asincrónica se completa. El creador del código de continuación tiene control sobre lo que ese código ejecuta. Puede crear el código de continuación explícitamente, mediante métodos de la clase Task (por ejemplo, ContinueWith) o implícitamente, usando la compatibilidad con lenguaje sobre las continuaciones (por ejemplo, await en C#, Await en Visual Basic, AwaitValue en F#).

Estado de la tarea

La clase Task proporciona un ciclo de vida para las operaciones asincrónicas y ese ciclo se representa mediante la enumeración TaskStatus. Para admitir los casos extremos de tipos que se derivan de Task y Task<TResult>, y para admitir la separación de la construcción de la programación, la clase Task expone un método Start. Las tareas creadas por los constructores públicos Task se denominan tareas en frío, porque inician su ciclo de vida en el estado Created no programado y solo se programan cuando se llama a Start en estas instancias.

Todas las demás tareas inician su ciclo de vida en un estado activo, lo que significa que las operaciones asincrónicas que representan ya se han iniciado y su estado de la tarea es un valor de enumeración distinto de TaskStatus.Created. Todas las tareas que se devuelven de métodos de TAP deben estar activas. Si un método de TAP usa internamente el constructor de una tarea para crear instancias de la tarea que se va a devolver, el método de TAP debe llamar a Start en el objeto Task antes de devolverlo. Los consumidores de un método de TAP pueden suponer con seguridad que la tarea devuelta está activa y no deben intentar llamar a Start en ningún Task que se devuelve de un método de TAP. La llamada a Start en una tarea activa produce una excepción InvalidOperationException.

Cancelación (opcional)

En TAP, la cancelación es opcional tanto para los implementadores de método asincrónico como para los consumidores de este método. Si una operación permite la cancelación, expone una sobrecarga del método asincrónico que acepta un token de cancelación (instancia de CancellationToken). Por convención, el parámetro se denomina cancellationToken.

public Task ReadAsync(byte [] buffer, int offset, int count,
                      CancellationToken cancellationToken)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          cancellationToken As CancellationToken) _
                          As Task

La operación asincrónica supervisa este token para las solicitudes de cancelación. Si recibe una solicitud de cancelación, puede elegir admitir esa solicitud y cancelar la operación. Si la solicitud de cancelación hace que el trabajo finalice prematuramente, el método de TAP devuelve una tarea que finaliza en el estado Canceled; no hay ningún resultado disponible y no se produce ninguna excepción. El estado Canceled se considera un estado final (completado) para una tarea, junto con los estados Faulted y RanToCompletion. Por tanto, si una tarea está en el estado Canceled, su propiedad IsCompleted devuelve true. Cuando una tarea se completa en el estado Canceled, cualquier continuación registrada con la tarea se programa o se ejecuta, a menos que se especificara una opción de continuación como NotOnCanceled para rechazar la continuación. Cualquier código que espera de forma asincrónica una tarea cancelada mediante el uso de características del lenguaje sigue ejecutándose pero recibe un objeto OperationCanceledException o una excepción derivada del mismo. El código que se bloquea sincrónicamente en espera de la tarea mediante métodos como Wait y WaitAll también continúa ejecutándose con una excepción.

Si un token de cancelación ha solicitado la cancelación antes de que se llame al método de TAP que acepta ese token, el método de TAP debe devolver una tarea Canceled. Sin embargo, si se solicita la cancelación mientras la operación asincrónica se ejecuta, la operación asincrónica no necesita aceptar la solicitud de cancelación. La tarea devuelta debe finalizar en el estado Canceled solo si la operación termina como resultado de la solicitud de cancelación. Si se solicita la cancelación pero aún se produce un resultado o una excepción, la tarea debe finalizar en el estado RanToCompletion o Faulted.

En el caso de los métodos asincrónicos que quieren exponer la capacidad de cancelarse ante todo, no tiene que proporcionar una sobrecarga que no acepte un token de cancelación. Para los métodos que no pueden cancelarse, no proporcione sobrecargas que acepten un token de cancelación; esto ayuda a indicar al llamador si el método de destino es realmente cancelable. El código de consumidor que no desea la cancelación puede llamar a un método que acepta un objeto CancellationToken y proporciona None como valor del argumento. None es funcionalmente equivalente al objeto CancellationToken predeterminado.

Informe de progreso (opcional)

Algunas operaciones asincrónicas se benefician de proporcionar notificaciones de progreso; se suelen usar para actualizar una interfaz de usuario con información sobre el progreso de la operación asincrónica.

En TAP, el progreso se controla a través de una interfaz IProgress<T>, la cual se pasa al método asincrónico como un parámetro normalmente denominado progress. Proporcionar la interfaz de progreso cuando se llama al método asincrónico ayuda a eliminar condiciones de carrera resultantes de un uso incorrecto (es decir, cuando los controladores de eventos registrados incorrectamente después del inicio de la operación pueden perder actualizaciones). Lo que es más importante, la interfaz de progreso admite implementaciones diferentes de progreso, según determina el código usado. Por ejemplo, el código usado puede que desee encargarse solo de la última actualización de progreso, almacenar en búfer todas las actualizaciones, invocar una acción para cada actualización o controlar si se serializa la invocación en un subproceso determinado. Todas estas opciones se pueden conseguir mediante otra implementación de la interfaz, personalizada según las necesidades particulares del consumidor. Como ocurre con la cancelación, las implementaciones de TAP deben proporcionar un parámetro IProgress<T> solo si la API admite notificaciones de progreso.

Por ejemplo, si el método ReadAsync anteriormente mencionado en este artículo puede informar del progreso intermedio en forma de número de bytes leídos hasta el momento, la devolución de llamada de progreso puede ser una interfaz IProgress<T>:

public Task ReadAsync(byte[] buffer, int offset, int count,
                      IProgress<long> progress)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          progress As IProgress(Of Long)) As Task

Si un método FindFilesAsync devuelve una lista de todos los archivos que satisfacen un patrón particular de búsqueda, la devolución de progreso puede proporcionar una estimación del porcentaje de trabajo completado y el conjunto actual de resultados parciales. Podría proporcionar esta información con una tupla:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
            string pattern,
            IProgress<Tuple<double,
            ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

o con un tipo de datos que sea específico de la API:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of FindFilesProgressInfo)) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

En este último caso, el tipo de datos especial suele tener el sufijo ProgressInfo.

Si las implementaciones de TAP proporcionan sobrecargas que aceptan un parámetro progress, deben permitir que el argumento sea null, en cuyo caso no se notificará ningún progreso. Las implementaciones de TAP deben notificar sincrónicamente el progreso al objeto Progress<T>, lo que permite que el método asincrónico proporcione rápidamente el progreso. También permite al consumidor del progreso determinar cómo y dónde es mejor controlar la información. Por ejemplo, la instancia de progreso puede optar por hacerse con las devoluciones de llamada y generar eventos en un contexto capturado de sincronización.

Implementaciones de IProgress<T>

.NET proporciona la clase Progress<T>, que implementa IProgress<T>. La clase Progress<T> se declara de la forma siguiente:

public class Progress<T> : IProgress<T>  
{  
    public Progress();  
    public Progress(Action<T> handler);  
    protected virtual void OnReport(T value);  
    public event EventHandler<T>? ProgressChanged;  
}  

Una instancia de Progress<T> expone un evento ProgressChanged, que se provoca cada vez que la operación asincrónica informe de una actualización de progreso. El evento ProgressChanged se genera en el objeto SynchronizationContext que se capturó cuando se creó la instancia de Progress<T>. Si no había ningún contexto de sincronización disponible, se usa un contexto predeterminado destinado al grupo de subprocesos. Los controladores pueden registrarse con este evento. También se puede proporcionar un único controlador al constructor Progress<T> por comodidad, y se comportará como un controlador de eventos para el evento ProgressChanged. Las actualizaciones de progreso se generan de forma asincrónica para evitar retrasar la operación asincrónica mientras los controladores de eventos se ejecutan. Otra implementación IProgress<T> podría elegir aplicar semánticas diferentes.

Elección de las sobrecargas que se van a proporcionar

Si una implementación TAP usa la propiedad CancellationToken y los parámetros IProgress<T> opcionales, podría requerir hasta cuatro sobrecargas:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Aunque muchas implementaciones de TAP no ofrecen funcionalidades de cancelación o progreso, por lo que requieren un único método:

public Task MethodNameAsync(…);  
Public MethodNameAsync(…) As Task  

Si una implementación de TAP admite la cancelación o el progreso pero no ambos, puede proporcionar dos sobrecargas:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
  
// … or …  
  
public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task  
  
' … or …  
  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task  

Si una implementación TAP admite la cancelación y el progreso, puede exponer las cuatro sobrecargas. Sin embargo, puede proporcionar solo las dos siguientes:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Para compensar las dos combinaciones intermedias que faltan, los desarrolladores pueden pasar la propiedad None o un objeto CancellationToken predeterminado para el parámetro cancellationToken y null para el parámetro progress.

Si se espera que cada uso del método TAP admita cancelación o progreso, puede omitir las sobrecargas que no acepten el parámetro pertinente.

Si se decide exponer varias sobrecargas para conseguir que la cancelación o el progreso sean opcionales, las sobrecargas que no admitan cancelación o progreso deben comportarse como si pasaran None para la cancelación o null para el progreso en la sobrecarga que admite ambas.

Title Descripción
Patrones para la programación asincrónica Presenta los tres patrones para realizar las operaciones asincrónicas: el patrón asincrónico basado en tareas (TAP), el modelo de programación asincrónica (APM) y el patrón asincrónico basado en eventos (EAP).
Implementar el modelo asincrónico basado en tareas Describe cómo implementar el patrón asincrónico basado en tareas (TAP) de tres maneras: mediante los compiladores de C# y Visual Basic de Visual Studio, manualmente o a través de una combinación del compilador y de métodos manuales.
Modelo asincrónico basado en tareas (TAP) Describe cómo se pueden utilizar las tareas y las devoluciones de llamada para conseguir esperas sin bloqueos.
Interoperabilidad con otros tipos y patrones asincrónicos Describe cómo usar el patrón asincrónico basado en tareas (TAP) para implementar el modelo de programación asincrónica (APM) y el patrón asincrónico basado en eventos (EAP).