Compartir a través de


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> en el espacio de nombres System.Threading.Tasks, que se usan para representar operaciones asincrónicas.

Nomenclatura, parámetros y tipos de valor devuelto

TAP usa un único 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ónica (APM o IAsyncResult) y el patrón asincrónico basado en eventos (EAP). APM requiere Begin y End métodos. EAP requiere un método que tenga el sufijo Async y también requiere uno o varios eventos, tipos delegados para manejadores de eventos y tipos derivados de EventArg. Los métodos asincrónicos de TAP incluyen el Async sufijo después del nombre de la operación para los métodos que devuelven tipos que se pueden esperar, como Task, Task<TResult>, ValueTasky ValueTask<TResult>. Por ejemplo, una operación asincrónica Get que devuelve un Task<String> objeto se puede denominar GetAsync. Si va a agregar un método TAP a una clase que ya contiene un nombre de método EAP con el Async sufijo, use el sufijo TaskAsync en su lugar. Por ejemplo, si la clase ya tiene un GetAsync método , use el nombre GetTaskAsync. Si un método inicia una operación asincrónica pero no devuelve un tipo esperable, su nombre debe comenzar con Begin, Starto algún otro verbo para sugerir que este método no devuelve ni produce el resultado de la operación.  

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

Los parámetros de un método TAP deben coincidir con los parámetros de su homólogo sincrónico y deben proporcionarse en el mismo orden. Sin embargo, out y ref los parámetros están exentos de esta regla y deben evitarse por completo. Los datos que se habrían devuelto a través de un parámetro out o ref se deberían devolver como parte del TResult devuelto por Task<TResult>, y se debería usar una tupla o una estructura de datos personalizada para acomodar múltiples valores. Además, considere la posibilidad de agregar un CancellationToken parámetro incluso si el homólogo sincrónico del método TAP no ofrece uno.

Los métodos dedicados exclusivamente a la creación, manipulación o combinación de tareas (donde la intención asincrónica del método está clara en el nombre del método o en el nombre del tipo al que pertenece el método) no deben seguir este patrón de nomenclatura; estos métodos se conocen a menudo como combinadores. Entre los ejemplos de combinadores se incluyen WhenAll y WhenAny, y se describen en la sección Uso de combinadores integrados basados en tareas del artículo Consumo del patrón asincrónico basado en tareas.

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

Iniciar una operación asincrónica

Un método asincrónico basado en TAP puede realizar una pequeña cantidad de trabajo de forma sincrónica, como validar argumentos e iniciar la operación asincrónica, antes de devolver la tarea resultante. El trabajo sincrónico debe mantenerse como mínimo para que el método asincrónico pueda devolverse rápidamente. Entre los motivos de un retorno rápido se incluyen los siguientes:

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

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

En algunos casos, la cantidad de trabajo necesaria para completar la operación es menor que la cantidad de trabajo necesaria para iniciar la operación de forma asincrónica. Leer de un flujo donde la operación de lectura puede ser satisfecha por datos que ya están almacenados en memoria es un ejemplo de tal escenario. En tales casos, 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 el código de producción. Por ejemplo, si pasar una referencia nula (Nothing en Visual Basic) como uno de los argumentos del método provoca un estado de error (normalmente representado por una ArgumentNullException excepción), puede modificar el código de llamada para asegurarse de que nunca se pasa 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, incluso si el método asincrónico se completa de forma sincrónica 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 sola tarea.

Entorno de destino

Al implementar un método 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 para la mayoría de la ejecución de la operación), ejecutarla en un subproceso específico (como el subproceso de interfaz de usuario) o usar cualquier número de contextos potenciales. Un método TAP puede incluso no tener nada que ejecutar y puede devolver un Task que representa la aparición de una condición en otro lugar del sistema (por ejemplo, una tarea que representa los 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 dónde se ejecuta ese código. Puede crear el código de continuación explícitamente, a través de métodos de la Task clase (por ejemplo, ContinueWith) o implícitamente, mediante la compatibilidad de lenguaje basada en continuaciones (por ejemplo, await en C#, Await en Visual Basic, AwaitValue en F#).

Estado de la tarea

La Task clase proporciona un ciclo de vida para las operaciones asincrónicas y ese ciclo se representa mediante la TaskStatus enumeración . 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 conocen como tareas inactivas, ya que comienzan su ciclo de vida en el estado no programado Created y solo se programan cuando Start se llama a en estas instancias.

Todas las demás tareas comienzan 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 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 TAP usa internamente el constructor de una tarea para crear instancias de la tarea que se va a devolver, el método TAP debe llamar a Start en el Task objeto antes de devolverla. 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étodos asincrónicos como para los consumidores de métodos asincrónicos. Si una operación permite la cancelación, expone una sobrecarga del método asincrónico que acepta un token de cancelación (CancellationToken instancia). 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 monitorea este token para detectar solicitudes de cancelación. Si recibe una solicitud de cancelación, puede optar por respetar esa solicitud y cancelar la operación. Si la solicitud de cancelación da como resultado que el trabajo finaliza prematuramente, el método TAP devuelve una tarea que termina en el Canceled estado; no hay ningún resultado disponible y no se produce ninguna excepción. El Canceled estado se considera un estado final (completado) para una tarea, junto con los Faulted estados y RanToCompletion . Por lo tanto, si una tarea está en estado Canceled , su IsCompleted propiedad devuelve true. Cuando una tarea se completa en el Canceled estado, las continuaciones registradas con la tarea se programan o ejecutan, a menos que se haya especificado una opción de continuación como NotOnCanceled para optar por no continuar. Cualquier código que esté esperando de forma asincrónica una tarea cancelada mediante el uso de características del lenguaje continúa ejecutándose, pero recibe un OperationCanceledException o una excepción derivada de este. Código que se bloquea sincrónicamente esperando la tarea a través de 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 se ejecuta la operación asincrónica, la operación asincrónica no necesita aceptar la solicitud de cancelación. La tarea devuelta debe terminar en el Canceled estado solo si la operación finaliza como resultado de la solicitud de cancelación. Si se solicita la cancelación, pero se sigue produciendo 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. En el caso de los métodos que no se pueden cancelar, no proporcione sobrecargas que acepten un token de cancelación; esto ayuda a indicar al autor de la llamada 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 valor predeterminado CancellationToken.

Informes de progreso (opcional)

Algunas operaciones asincrónicas se benefician de proporcionar notificaciones de progreso; normalmente se usan 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 IProgress<T> interfaz, que se pasa al método asíncrono como un parámetro que normalmente se llama 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 más importante es que la interfaz de progreso admita distintas implementaciones de progreso, según lo determinado por el código de consumo. 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 lograr mediante una implementación diferente de la interfaz, personalizada a las necesidades del consumidor en particular. Al igual que con la cancelación, las implementaciones de TAP solo deben proporcionar un IProgress<T> parámetro 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 cumplen con un determinado patrón de búsqueda, la llamada de retorno de progreso podría 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 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, al tipo de datos especial se le añade el sufijo ProgressInfo.

Si las implementaciones de TAP proporcionan sobrecargas que aceptan un progress parámetro, deben permitir que el argumento sea null, en cuyo caso no se notifica ningún progreso. Las implementaciones de TAP deben notificar el progreso al Progress<T> objeto de forma sincrónica, lo que permite al método asincrónico proporcionar rápidamente el progreso. También permite al consumidor del progreso determinar cómo y dónde 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 Progress<T> clase , que implementa IProgress<T>. La Progress<T> clase se declara de la siguiente manera:

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 activa cada vez que la operación asincrónica notifica 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 Progress<T> constructor para mayor comodidad y se comporta igual que un controlador de eventos para el ProgressChanged evento. Las actualizaciones de progreso se generan asincrónicamente para evitar retrasar la operación asincrónica mientras se ejecutan los controladores de eventos. Otra IProgress<T> implementación podría optar por aplicar una semántica diferente.

Elección de las sobrecargas que se van a proporcionar

Si una implementación de TAP usa los parámetros opcionales CancellationToken y opcionales IProgress<T> , 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  

Sin embargo, muchas implementaciones de TAP no proporcionan 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, solo puede proporcionar los 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 None o un valor predeterminado CancellationToken para el cancellationToken parámetro y null para el progress parámetro .

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.

Título Descripción
Patrones de programación asincrónicos Presenta los tres patrones para realizar 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).
Implementación del patrón asincrónico basado en tareas Describe cómo implementar el patrón asincrónico basado en tareas (TAP) de tres maneras: mediante el uso de los compiladores de C# y Visual Basic en Visual Studio, manualmente o mediante una combinación de los métodos manuales y del compilador.
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 modelo asincrónico basado en eventos (EAP).