Compartir a través de


Implementación del patrón asincrónico basado en tareas

Puede 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 compiladores y manuales. En las secciones siguientes se describe cada método con detalle. Puede usar el patrón TAP para implementar operaciones asincrónicas dependientes de la computación y dependientes de E/S. En la sección Cargas de trabajo se describe cada tipo de operación.

Generación de métodos TAP

Uso de los compiladores

A partir de .NET Framework 4.5, cualquier método que se atribuye a la async palabra clave (Async en Visual Basic) se considera un método asincrónico y los compiladores de C# y Visual Basic realizan las transformaciones necesarias para implementar el método de forma asincrónica mediante TAP. Un método asincrónico debe devolver un System.Threading.Tasks.Task objeto o System.Threading.Tasks.Task<TResult> . Para este último, el cuerpo de la función debe devolver un TResulty el compilador garantiza que este resultado esté disponible a través del objeto de tarea resultante. Del mismo modo, las excepciones que no se controlan dentro del cuerpo del método se propagan a la tarea de salida y hacen que la tarea resultante finalice en el estado TaskStatus.Faulted. La excepción a esta regla es cuando un OperationCanceledException (o tipo derivado) queda sin gestionar, en cuyo caso la tarea resultante termina en el estado TaskStatus.Canceled.

Generación manual de métodos TAP

Puede implementar el patrón TAP manualmente para un mejor control sobre la implementación. El compilador se basa en el área de superficie pública expuesta del espacio de nombres System.Threading.Tasks y los tipos auxiliares del espacio de nombres System.Runtime.CompilerServices. Para implementar el TAP usted mismo, cree un objeto TaskCompletionSource<TResult>, realice la operación asincrónica y, cuando se complete, llame al método SetResult, SetException o bien SetCanceled, o a la versión Try de uno de estos métodos. Al implementar manualmente un método TAP, debe completar la tarea resultante cuando se complete la operación asincrónica representada. Por ejemplo:

public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
{
    var tcs = new TaskCompletionSource<int>();
    stream.BeginRead(buffer, offset, count, ar =>
    {
        try { tcs.SetResult(stream.EndRead(ar)); }
        catch (Exception exc) { tcs.SetException(exc); }
    }, state);
    return tcs.Task;
}
<Extension()>
Public Function ReadTask(stream As Stream, buffer() As Byte,
                         offset As Integer, count As Integer,
                         state As Object) As Task(Of Integer)
    Dim tcs As New TaskCompletionSource(Of Integer)()
    stream.BeginRead(buffer, offset, count, Sub(ar)
                                                Try
                                                    tcs.SetResult(stream.EndRead(ar))
                                                Catch exc As Exception
                                                    tcs.SetException(exc)
                                                End Try
                                            End Sub, state)
    Return tcs.Task
End Function

Enfoque híbrido

Es posible que le resulte útil implementar el patrón TAP manualmente, pero delegar la lógica principal de la implementación en el compilador. Por ejemplo, es posible que desee usar el enfoque híbrido cuando desee comprobar argumentos fuera de un método asincrónico generado por el compilador para que las excepciones puedan escapar al autor de llamada directo del método en lugar de exponerse a través del System.Threading.Tasks.Task objeto :

public Task<int> MethodAsync(string input)
{
    if (input == null) throw new ArgumentNullException("input");
    return MethodAsyncInternal(input);
}

private async Task<int> MethodAsyncInternal(string input)
{

   // code that uses await goes here

   return value;
}
Public Function MethodAsync(input As String) As Task(Of Integer)
    If input Is Nothing Then Throw New ArgumentNullException("input")

    Return MethodAsyncInternal(input)
End Function

Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)

    ' code that uses await goes here

    return value
End Function

Otro caso en el que esta delegación es útil es cuando se implementa la optimización de ruta de acceso rápida y se quiere devolver una tarea almacenada en caché.

Carga de trabajo

Puede implementar operaciones asincrónicas limitadas por el cálculo y limitadas por E/S como métodos TAP. Sin embargo, cuando los métodos TAP se exponen públicamente desde una biblioteca, solo deben proporcionarse para cargas de trabajo que impliquen operaciones enlazadas a E/S (también pueden implicar cálculos, pero no deben ser puramente computacionales). Si un método está puramente limitado por el cálculo, solo debe exponerse como una implementación sincrónica. Después, el código que lo consume puede elegir si encapsular una invocación de ese método sincrónico en una tarea para descargar el trabajo a otro subproceso o para lograr paralelismo. Y si un método está enlazado a E/S, solo debe exponerse como una implementación asincrónica.

Tareas limitadas por el cálculo

La System.Threading.Tasks.Task clase es ideal para representar operaciones de uso intensivo de cálculo. De forma predeterminada, aprovecha la compatibilidad especial dentro de la ThreadPool clase para proporcionar una ejecución eficaz y también proporciona un control significativo sobre cuándo, dónde y cómo se ejecutan los cálculos asincrónicos.

Puede generar tareas limitadas por la computación de las siguientes maneras:

  • En .NET Framework 4.5 y versiones posteriores (incluido .NET Core y .NET 5+), use el método estático Task.Run como acceso directo a TaskFactory.StartNew. Puede usar Run para iniciar fácilmente una tarea vinculada al cálculo que tenga como destino el grupo de subprocesos. Este es el mecanismo preferido para iniciar una tarea intensiva en cálculo. Use StartNew directamente solo cuando desee un control más específico sobre la tarea.

  • En .NET Framework 4, use el método TaskFactory.StartNew, que acepta un delegado (normalmente un Action<T> o un Func<TResult>) que se ejecutará de forma asincrónica. Si proporciona un Action<T> delegado, el método devuelve un System.Threading.Tasks.Task objeto que representa la ejecución asincrónica de ese delegado. Si proporciona un Func<TResult> delegado, el método devuelve un System.Threading.Tasks.Task<TResult> objeto . Las sobrecargas del StartNew método aceptan un token de cancelación (CancellationToken), opciones de creación de tareas (TaskCreationOptions) y un programador de tareas (TaskScheduler), todos los cuales proporcionan un control específico sobre la programación y ejecución de la tarea. Una instancia de fábrica que tiene como destino el programador de tareas actual está disponible como una propiedad estática (Factory) de la Task clase; por ejemplo: Task.Factory.StartNew(…).

  • Use los constructores del Task tipo y el Start método si desea generar y programar la tarea por separado. Los métodos públicos solo deben devolver tareas que ya han sido iniciadas.

  • Use las sobrecargas del método Task.ContinueWith. Este método crea una nueva tarea programada cuando se completa otra tarea. Algunas de las ContinueWith sobrecargas aceptan un token de cancelación, opciones para la continuación y un planificador de tareas para lograr un mejor control sobre la programación y la ejecución de la tarea de continuación.

  • Use los TaskFactory.ContinueWhenAll métodos y TaskFactory.ContinueWhenAny . Estos métodos crean una nueva tarea programada cuando se completa todo o cualquiera de un conjunto proporcionado de tareas. Estos métodos también proporcionan sobrecargas para controlar la programación y ejecución de estas tareas.

En las tareas enlazadas a proceso, el sistema puede impedir la ejecución de una tarea programada si recibe una solicitud de cancelación antes de empezar a ejecutar la tarea. Por lo tanto, si proporciona un token de cancelación (CancellationToken objeto), puede pasar ese token al código asincrónico que supervisa el token. También puede proporcionar el token a uno de los métodos previamente mencionados, como StartNew o Run, para que el entorno de ejecución de Task también pueda supervisar el token.

Por ejemplo, considere un método asincrónico que representa una imagen. El cuerpo de la tarea puede sondear el token de cancelación para que el código pueda salir pronto si llega una solicitud de cancelación durante la representación. Además, si la solicitud de cancelación llega antes de que se inicie la representación, querrá evitar la operación de representación:

internal Task<Bitmap> RenderAsync(
              ImageData data, CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        var bmp = new Bitmap(data.Width, data.Height);
        for(int y=0; y<data.Height; y++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            for(int x=0; x<data.Width; x++)
            {
                // render pixel [x,y] into bmp
            }
        }
        return bmp;
    }, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As _
                            CancellationToken) As Task(Of Bitmap)
    Return Task.Run(Function()
                        Dim bmp As New Bitmap(data.Width, data.Height)
                        For y As Integer = 0 to data.Height - 1
                            cancellationToken.ThrowIfCancellationRequested()
                            For x As Integer = 0 To data.Width - 1
                                ' render pixel [x,y] into bmp
                            Next
                        Next
                        Return bmp
                    End Function, cancellationToken)
End Function

Las tareas limitadas por el cálculo terminan en estado Canceled si se cumple al menos una de las siguientes condiciones:

  • Una solicitud de cancelación llega a través del CancellationToken objeto , que se proporciona como argumento para el método de creación (por ejemplo, StartNew o Run) antes de que la tarea pase al Running estado.

  • Una excepción OperationCanceledException no está controlada dentro del cuerpo de esta tarea, esa excepción contiene el mismo objeto CancellationToken que se pasa a la tarea y ese token muestra que se solicitó la cancelación.

Si otra excepción no se controla dentro del cuerpo de la tarea, la tarea finaliza en el Faulted estado y cualquier intento de esperar a que la tarea termine o acceder a su resultado hace que se produzca una excepción.

Tareas enlazadas a E/S

Para crear una tarea que no debe estar respaldada directamente por un subproceso para la totalidad de su ejecución, use el TaskCompletionSource<TResult> tipo . Este tipo expone una Task propiedad que devuelve una instancia asociada Task<TResult> . El ciclo de vida de esta tarea se controla mediante TaskCompletionSource<TResult> métodos como SetResult, SetException, SetCanceledy sus TrySet variantes.

Supongamos que desea crear una tarea que se completará después de un período de tiempo especificado. Por ejemplo, puede que desee retrasar una actividad en la interfaz de usuario. La clase System.Threading.Timer ya proporciona la capacidad de invocar de forma asincrónica un delegado después de un período de tiempo especificado, y TaskCompletionSource<TResult> le permite colocar un objeto Task<TResult> delante del temporizador, por ejemplo:

public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
    TaskCompletionSource<DateTimeOffset> tcs = null;
    Timer timer = null;

    timer = new Timer(delegate
    {
        timer.Dispose();
        tcs.TrySetResult(DateTimeOffset.UtcNow);
    }, null, Timeout.Infinite, Timeout.Infinite);

    tcs = new TaskCompletionSource<DateTimeOffset>(timer);
    timer.Change(millisecondsTimeout, Timeout.Infinite);
    return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset)
    Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
    Dim timer As Timer = Nothing

    timer = New Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(DateTimeOffset.UtcNow)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

El Task.Delay método se proporciona para este propósito y puede usarlo dentro de otro método asincrónico, por ejemplo, para implementar un bucle de sondeo asincrónico:

public static async Task Poll(Uri url, CancellationToken cancellationToken,
                              IProgress<bool> progress)
{
    while(true)
    {
        await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
        bool success = false;
        try
        {
            await DownloadStringAsync(url);
            success = true;
        }
        catch { /* ignore errors */ }
        progress.Report(success);
    }
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken,
                           progress As IProgress(Of Boolean)) As Task
    Do While True
        Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
        Dim success As Boolean = False
        Try
            await DownloadStringAsync(url)
            success = true
        Catch
            ' ignore errors
        End Try
        progress.Report(success)
    Loop
End Function

La TaskCompletionSource<TResult> clase no tiene un homólogo no genérico. Sin embargo, Task<TResult> deriva de Task, por lo que puede usar el objeto genérico TaskCompletionSource<TResult> para los métodos enlazados a E/S que simplemente devuelven una tarea. Para ello, puede usar un origen con un TResult ficticio (Boolean es una buena opción predeterminada, pero si le preocupa que el usuario de Task lo convierta en tipos inferiores a un objeto Task<TResult>, puede usar un tipo TResult privado en su lugar). Por ejemplo, el Delay método del ejemplo anterior devuelve la hora actual junto con el desplazamiento resultante (Task<DateTimeOffset>). Si este valor de resultado no es necesario, el método podría codificarse como se indica a continuación (tenga en cuenta el cambio de tipo de valor devuelto y el cambio de argumento a TrySetResult):

public static Task<bool> Delay(int millisecondsTimeout)
{
     TaskCompletionSource<bool> tcs = null;
     Timer timer = null;

     timer = new Timer(delegate
     {
         timer.Dispose();
         tcs.TrySetResult(true);
     }, null, Timeout.Infinite, Timeout.Infinite);

     tcs = new TaskCompletionSource<bool>(timer);
     timer.Change(millisecondsTimeout, Timeout.Infinite);
     return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of Boolean)
    Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
    Dim timer As Timer = Nothing

    Timer = new Timer(Sub(obj)
                          timer.Dispose()
                          tcs.TrySetResult(True)
                      End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)

    tcs = New TaskCompletionSource(Of Boolean)(timer)
    timer.Change(millisecondsTimeout, Timeout.Infinite)
    Return tcs.Task
End Function

Tareas enlazadas a cálculos y enlazadas a E/S mixtas

Los métodos asincrónicos no se limitan solo a las operaciones enlazadas a proceso o enlazadas a E/S, pero pueden representar una combinación de los dos. De hecho, varias operaciones asincrónicas a menudo se combinan en operaciones mixtas más grandes. Por ejemplo, el RenderAsync método de un ejemplo anterior realizó una operación de cálculo intensivo para representar una imagen basada en alguna entrada imageData. Esto imageData podría provenir de un servicio web al que accede de forma asincrónica:

public async Task<Bitmap> DownloadDataAndRenderImageAsync(
    CancellationToken cancellationToken)
{
    var imageData = await DownloadImageDataAsync(cancellationToken);
    return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(
             cancellationToken As CancellationToken) As Task(Of Bitmap)
    Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
    Return Await RenderAsync(imageData, cancellationToken)
End Function

En este ejemplo también se muestra cómo se puede incluir en un subproceso un único token de cancelación mediante varias operaciones asincrónicas. Para más información, vea la sección de uso de la cancelación en Utilizar el modelo asincrónico basado en tareas.

Consulte también