Compartir a través de


Utilizar el modelo asincrónico basado en tareas

Cuando utilizas el Patrón Asincrónico Basado en Tareas (TAP) para trabajar con operaciones asincrónicas, puedes emplear callbacks para lograr una espera sin bloqueo. En el caso de las tareas, esto se logra mediante métodos como Task.ContinueWith. El soporte asincrónico basado en lenguaje oculta los callbacks al permitir que las operaciones asincrónicas se esperen dentro del flujo de control normal, y el código generado por el compilador proporciona este mismo soporte a nivel de API.

Suspender la ejecución con Await

Puede usar la palabra clave await en C# y el operador Await en Visual Basic para esperar Task y Task<TResult> objetos de forma asincrónica. Cuando se espera una clase Task, la expresión await es de tipo void. Cuando se espera una clase Task<TResult>, la expresión await es de tipo TResult. Debe producirse una expresión await dentro del cuerpo de un método asincrónico. (Estas características de lenguaje se introdujeron en .NET Framework 4.5).

En realidad, la funcionalidad de await instala una devolución de llamada en la tarea mediante una continuación. Esta devolución de llamada reanuda el método asincrónico en el punto de suspensión. Cuando se reanuda el método asincrónico, si la operación de espera finalizó correctamente y fue un objeto Task<TResult>, se devuelve su TResult. Si las clases Task o Task<TResult> por la que esperaba finalizaron con el estado Canceled, se produce una excepción OperationCanceledException. Si las clases Task o Task<TResult> por la que esperaba finalizaron con el estado Faulted, se produce la excepción que causó el error. Un objeto Task puede producir un error como resultado de múltiples excepciones, pero solo una de estas excepciones se propaga. Sin embargo, la Task.Exception propiedad devuelve una AggregateException excepción que contiene todos los errores.

Si un contexto de sincronización (objeto SynchronizationContext) está asociado con el subproceso que ejecutaba el método asincrónico en el momento de la suspensión (por ejemplo, si la propiedad SynchronizationContext.Current no es null), el método asincrónico se reanuda en ese mismo contexto de sincronización con el método Post del contexto. De lo contrario, se basa en el programador de tareas (objeto TaskScheduler) que estaba vigente en el momento de la suspensión. Normalmente, este es el programador de tareas predeterminado (TaskScheduler.Default), que tiene como destino el grupo de subprocesos. Este programador de tareas determina si la operación asincrónica esperada debe reanudarse donde se completó o si se debe programar la reanudación. El programador predeterminado normalmente permite que la continuación se ejecute en el hilo donde finalizó la operación en espera.

Cuando se llama a un método asincrónico, ejecuta sincrónicamente el cuerpo de la función hasta que la primera expresión de espera en una instancia esperable que todavía no se ha completado, momento en el que la invocación se devuelve al llamador. Si el método asincrónico no devuelve void, se devuelve un Task objeto o Task<TResult> para representar el cálculo en curso. En un método asincrónico distinto de void, si se encuentra una instrucción de devolución o se alcanza el final del cuerpo del método, la tarea se completa en el estado final RanToCompletion. Si una excepción no controlada hace que el control salga del cuerpo del método asíncrono, la tarea finaliza en el estado Faulted. Si esa excepción es un OperationCanceledException, entonces la tarea finaliza en el estado Canceled. De esta manera, el resultado o la excepción se publican finalmente.

Hay varias variaciones importantes de este comportamiento. Por motivos de rendimiento, si una tarea ya se ha completado cuando se espera, no se cede el control y la función continúa ejecutándose. Además, volver al contexto original no siempre es el comportamiento deseado y se puede cambiar; esto se describe con más detalle en la sección siguiente.

Configuración de la suspensión y reanudación con Yield y ConfigureAwait

Varios métodos proporcionan más control sobre la ejecución de un método asincrónico. Por ejemplo, puede usar el Task.Yield método para introducir un punto de rendimiento en el método asincrónico:

public class Task : …
{
    public static YieldAwaitable Yield();
    …
}

Esto es equivalente a volver a registrar o programar de manera asincrónica al contexto actual.

Task.Run(async delegate
{
    for(int i=0; i<1000000; i++)
    {
        await Task.Yield(); // fork the continuation into a separate work item
        ...
    }
});

También puede usar el Task.ConfigureAwait método para controlar mejor la suspensión y reanudación en un método asincrónico. Como se mencionó anteriormente, de forma predeterminada, el contexto actual se captura en el momento en que se suspende un método asincrónico y ese contexto capturado se usa para invocar la continuación del método asincrónico tras la reanudación. En muchos casos, este es el comportamiento exacto que desea. En otros casos, es posible que no le preocupe el contexto de continuación y puede lograr un mejor rendimiento al evitar dichos registros en el contexto original. Para habilitar esto, use el método Task.ConfigureAwait para informar a la operación await que no capture ni se reanude en el contexto, pero que continúe la ejecución siempre que haya completado la operación asincrónica que se estaba esperando:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Cancelación de una operación asincrónica

A partir de .NET Framework 4, los métodos TAP que admiten la cancelación proporcionan al menos una sobrecarga que acepta un token de cancelación (CancellationToken objeto).

Un token de cancelación se crea mediante un origen de token de cancelación (objeto CancellationTokenSource). La propiedad Token del origen devuelve el token de cancelación que se señalará cuando se llame al método Cancel del origen. Por ejemplo, si desea descargar una sola página web y desea poder cancelar la operación, cree un CancellationTokenSource objeto, pase su token al método TAP y, a continuación, llame al método del Cancel origen cuando esté listo para cancelar la operación:

var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();

Para cancelar varias invocaciones asincrónicas, puede pasar el mismo token a todas las invocaciones:

var cts = new CancellationTokenSource();
    IList<string> results = await Task.WhenAll(from url in urls select DownloadStringTaskAsync(url, cts.Token));
    // at some point later, potentially on another thread
    …
    cts.Cancel();

O bien, puede pasar el mismo token a un subconjunto selectivo de operaciones:

var cts = new CancellationTokenSource();
    byte [] data = await DownloadDataAsync(url, cts.Token);
    await SaveToDiskAsync(outputPath, data, CancellationToken.None);
    … // at some point later, potentially on another thread
    cts.Cancel();

Importante

Las solicitudes de cancelación se pueden iniciar desde cualquier hilo.

Puede pasar el CancellationToken.None valor a cualquier método que acepte un token de cancelación para indicar que nunca se solicitará la cancelación. Esto hace que la CancellationToken.CanBeCanceled propiedad devuelva falsey el método llamado puede optimizar en consecuencia. Con fines de prueba, también puede pasar un token de cancelación cancelado previamente cuyas instancias se crean mediante el constructor que acepta un valor booleano para indicar si el token debe iniciarse en un estado ya cancelado o que no se puede cancelar.

Este enfoque para la cancelación tiene varias ventajas:

  • Puede pasar el mismo token de cancelación a cualquier número de operaciones asincrónicas y sincrónicas.

  • La misma solicitud de cancelación puede extenderse a cualquier número de agentes de escucha.

  • El desarrollador de la API asincrónica está en control completo de si se puede solicitar la cancelación y cuándo puede surtir efecto.

  • El código que consume la API puede determinar selectivamente las invocaciones asincrónicas a las que se propagarán las solicitudes de cancelación.

Progreso de la supervisión

Algunos métodos asincrónicos exponen el progreso a través de una interfaz de progreso pasada al método asincrónico. Por ejemplo, considere una función que descarga de forma asincrónica una cadena de texto y, a lo largo del proceso, genera actualizaciones de progreso que incluyen el porcentaje de la descarga que se ha completado hasta ahora. Este método podría consumirse en una aplicación de Windows Presentation Foundation (WPF) como se indica a continuación:

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
            new Progress<int>(p => pbDownloadProgress.Value = p));
    }
    finally { btnDownload.IsEnabled = true; }
}

Uso de los combinadores integrados basados en tareas

El espacio de nombres System.Threading.Tasks incluye varios métodos para crear tareas y trabajar con ellas.

Task.Run

La clase Task incluye varios métodos Run que permiten delegar fácilmente el trabajo como Task o Task<TResult> en el grupo de subprocesos, por ejemplo:

public async void button1_Click(object sender, EventArgs e)
{
    textBox1.Text = await Task.Run(() =>
    {
        // … do compute-bound work here
        return answer;
    });
}

Algunos de estos métodos Run, como la sobrecarga Task.Run(Func<Task>), existen como forma abreviada del método TaskFactory.StartNew. Esta sobrecarga habilitan el uso de await en el trabajo descargado; por ejemplo:

public async void button1_Click(object sender, EventArgs e)
{
    pictureBox1.Image = await Task.Run(async() =>
    {
        using(Bitmap bmp1 = await DownloadFirstImageAsync())
        using(Bitmap bmp2 = await DownloadSecondImageAsync())
        return Mashup(bmp1, bmp2);
    });
}

Estas sobrecargas son lógicamente equivalentes a usar el TaskFactory.StartNew método junto con el Unwrap método de extensión en la biblioteca paralela de tareas.

Task.FromResult

Utilice el FromResult método en escenarios donde los datos ya estén disponibles y solo necesiten devolverse desde un método que devuelve tareas elevado a un Task<TResult>:

public Task<int> GetValueAsync(string key)
{
    int cachedValue;
    return TryGetCachedValue(out cachedValue) ?
        Task.FromResult(cachedValue) :
        GetValueAsyncInternal();
}

private async Task<int> GetValueAsyncInternal(string key)
{
    …
}

Task.WhenAll

Utilice el método WhenAll para esperar asincrónicamente en varias operaciones asincrónicas que se representan como tareas. El método tiene varias sobrecargas que admiten un conjunto de tareas no genéricas o un conjunto no uniforme de tareas genéricas (por ejemplo, esperar de forma asincrónica varias operaciones de retorno nulo o esperar de forma asincrónica varios métodos que devuelven valores donde cada valor puede tener un tipo diferente) y también admite un conjunto uniforme de tareas genéricas (como esperar de forma asincrónica varios métodos que devuelven TResult).

Supongamos que quiere enviar mensajes de correo electrónico a varios clientes. Puede superponer el envío de los mensajes para que no espere a que se complete un mensaje antes de enviar el siguiente. También puede averiguar cuándo se han completado las operaciones de envío y si se han producido errores:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);

Este código no controla explícitamente las excepciones que pueden producirse, pero permite que las excepciones se propaguen fuera de await de la tarea resultante de WhenAll. Para controlar las excepciones, puede usar código como el siguiente:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    ...
}

En este caso, si se produce un error en alguna operación asincrónica, todas las excepciones se consolidarán en una AggregateException excepción, que se almacena en el Task que se devuelve desde el WhenAll método . Sin embargo, solo una de esas excepciones es propagada por la palabra clave await. Si desea examinar todas las excepciones, puede volver a escribir el código anterior de la siguiente manera:

Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Veamos un ejemplo de cómo descargar varios archivos desde la web de forma asincrónica. En este caso, todas las operaciones asincrónicas tienen tipos de resultados homogéneos y es fácil acceder a los resultados:

string [] pages = await Task.WhenAll(
    from url in urls select DownloadStringTaskAsync(url));

Puede utilizar las mismas técnicas de control de excepciones que se explicaron en el escenario anterior que devuelve void:

Task<string> [] asyncOps =
    (from url in urls select DownloadStringTaskAsync(url)).ToArray();
try
{
    string [] pages = await Task.WhenAll(asyncOps);
    ...
}
catch(Exception exc)
{
    foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Task.WhenAny

Puede usar el WhenAny método para esperar de forma asincrónica a que solo una de varias operaciones asincrónicas que estén representadas como tareas se complete. Este método sirve cuatro casos de uso principales:

  • Redundancia: realizar una operación varias veces y seleccionar la que se completa primero (por ejemplo, ponerse en contacto con varios servicios web de cotizaciones que producirán un único resultado y seleccionar el que complete la más rápida).

  • Intercalación: iniciar varias operaciones y esperar que se completen todas, pero procesarlas a medida que se completan.

  • Limitación: permitir que operaciones adicionales comiencen a medida que otras se completan. Esto es una extensión del escenario de intercalación.

  • Recursividad temprana: por ejemplo, una operación representada por la tarea t1 puede agruparse en una tarea WhenAny con otra tarea t2, y puede esperar a la tarea WhenAny. La tarea t2 podría representar un tiempo de espera o una cancelación, o alguna otra señal que hace que la WhenAny tarea se complete antes de que se complete t1.

Redundancia

Considere un caso en el que quiera tomar una decisión sobre si desea comprar una acción. Hay varios servicios web de recomendación de existencias que confía, pero en función de la carga diaria, cada servicio puede acabar siendo lento en momentos diferentes. Puede usar el WhenAny método para recibir una notificación cuando se complete cualquier operación:

var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol),
    GetBuyRecommendation2Async(symbol),
    GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) BuyStock(symbol);

A diferencia WhenAllde , que devuelve los resultados desencapsulados de todas las tareas que se completaron correctamente, WhenAny devuelve la tarea que se completó. Si se produce un error en una tarea, es importante saber que se produjo un error y, si una tarea se realiza correctamente, es importante saber a qué tarea está asociada el valor devuelto. Por lo tanto, debe tener acceso al resultado de la tarea devuelta o esperarla aún más, como se muestra en este ejemplo.

Al igual que con WhenAll, tiene que ser capaz de dar cabida a excepciones. Dado que recibe de vuelta la tarea de completa, puede esperar que se hayan propagado los errores en la tarea devuelta y try/catch adecuadamente; por ejemplo:

Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
    Task<bool> recommendation = await Task.WhenAny(recommendations);
    try
    {
        if (await recommendation) BuyStock(symbol);
        break;
    }
    catch(WebException exc)
    {
        recommendations.Remove(recommendation);
    }
}

Además, incluso si una primera tarea se completa correctamente, es posible que se produzca un error en las tareas posteriores. En este momento, tiene varias opciones para tratar las excepciones: puede esperar hasta que se hayan completado todas las tareas iniciadas, en cuyo caso puede usar el WhenAll método o puede decidir que todas las excepciones son importantes y deben registrarse. Para ello, puede usar continuaciones para recibir una notificación cuando las tareas se hayan completado de forma asincrónica:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => { if (t.IsFaulted) Log(t.Exception); });
}

o:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}

o incluso:

private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
    foreach(var task in tasks)
    {
        try { await task; }
        catch(Exception exc) { Log(exc); }
    }
}
…
LogCompletionIfFailed(recommendations);

Por último, puede que desee cancelar todas las operaciones restantes:

var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol, cts.Token),
    GetBuyRecommendation2Async(symbol, cts.Token),
    GetBuyRecommendation3Async(symbol, cts.Token)
};

Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) BuyStock(symbol);

Intercalación

Considere un caso en el que va a descargar imágenes de la web y procesar cada imagen (por ejemplo, agregar la imagen a un control de interfaz de usuario). Procesa las imágenes secuencialmente en el subproceso de interfaz de usuario, pero quiere descargar las imágenes lo más simultáneamente como sea posible. Además, no quiere retrasar la adición de imágenes a la interfaz hasta que se hayan descargado todas. En su lugar, desea agregarlas a medida que se completan.

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

También puede aplicar la intercalación en un escenario que implica el procesamiento de cálculo intensivo en la clase ThreadPool de las imágenes descargadas; por ejemplo:

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)
         .ContinueWith(t => ConvertImage(t.Result)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

Limitaciones

Considere el ejemplo de intercalación, excepto en que el usuario descarga tantas imágenes que las descargas tienen que limitarse; por ejemplo, desea que solo un número específico de descargas ocurra al mismo tiempo. Para lograrlo, puede iniciar un subconjunto de las operaciones asincrónicas. A medida que se completen las operaciones, puede iniciar operaciones adicionales para ocupar su lugar.

const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
    imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
    nextIndex++;
}

while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch(Exception exc) { Log(exc); }

    if (nextIndex < urls.Length)
    {
        imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
        nextIndex++;
    }
}

Salida anticipada

Tenga en cuenta que espera de forma asincrónica para que se complete una operación mientras responde simultáneamente a la solicitud de cancelación de un usuario (por ejemplo, el usuario ha clic en un botón cancelar). En el código siguiente se muestra este escenario:

private CancellationTokenSource m_cts;

public void btnCancel_Click(object sender, EventArgs e)
{
    if (m_cts != null) m_cts.Cancel();
}

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();
    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        if (imageDownload.IsCompleted)
        {
            Bitmap image = await imageDownload;
            panel.AddImage(image);
        }
        else imageDownload.ContinueWith(t => Log(t));
    }
    finally { btnRun.Enabled = true; }
}

private static async Task UntilCompletionOrCancellation(
    Task asyncOp, CancellationToken ct)
{
    var tcs = new TaskCompletionSource<bool>();
    using(ct.Register(() => tcs.TrySetResult(true)))
        await Task.WhenAny(asyncOp, tcs.Task);
    return asyncOp;
}

Esta implementación permite volver a la interfaz de usuario en cuanto decide abandonar la operación, pero no se cancelan las operaciones asincrónicas subyacentes. Otra alternativa sería cancelar las operaciones pendientes cuando decide abandone la operación, pero no se restablece la interfaz de usuario hasta que las operaciones se hayan finalizado, posiblemente debido a una finalización anticipada debido a la solicitud de cancelación:

private CancellationTokenSource m_cts;

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();

    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text, m_cts.Token);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        Bitmap image = await imageDownload;
        panel.AddImage(image);
    }
    catch(OperationCanceledException) {}
    finally { btnRun.Enabled = true; }
}

Otro ejemplo de rescate anticipado implica el uso del WhenAny método junto con el Delay método , como se describe en la sección siguiente.

Task.Delay

Puede usar el Task.Delay método para introducir pausas en la ejecución de un método asincrónico. Esto es útil para muchos tipos de funcionalidades, incluidas la creación de bucles de sondeo y el retrasar el manejo de la entrada del usuario durante un período de tiempo predeterminado. El método Task.Delay también puede resultar útil junto con Task.WhenAny para implementar tiempos de espera con awaits.

Si una tarea que forma parte de una operación asincrónica mayor (por ejemplo, un servicio web de ASP.NET) tarda demasiado tiempo en completarse, la operación general podría sufrir, especialmente si no se completa alguna vez. Por este motivo, es importante poder agotar el tiempo de espera al esperar a una operación asincrónica. Los métodos sincrónicos Task.Wait, Task.WaitAll y Task.WaitAny aceptan valores de tiempo de espera, pero los métodos correspondientes TaskFactory.ContinueWhenAll, /, TaskFactory.ContinueWhenAny y los mencionados anteriormente Task.WhenAll, /, Task.WhenAny no. En su lugar, puede usar Task.Delay y Task.WhenAny en combinación para implementar un tiempo de espera.

Por ejemplo, en la aplicación de interfaz de usuario, supongamos que desea descargar una imagen y deshabilitar la interfaz de usuario mientras se descarga la imagen. Sin embargo, si la descarga tarda demasiado tiempo, quiere volver a habilitar la interfaz de usuario y descartar la descarga:

public async void btnDownload_Click(object sender, EventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap> download = GetBitmapAsync(url);
        if (download == await Task.WhenAny(download, Task.Delay(3000)))
        {
            Bitmap bmp = await download;
            pictureBox.Image = bmp;
            status.Text = "Downloaded";
        }
        else
        {
            pictureBox.Image = null;
            status.Text = "Timed out";
            var ignored = download.ContinueWith(
                t => Trace("Task finally completed"));
        }
    }
    finally { btnDownload.Enabled = true; }
}

Lo mismo se aplica a varias descargas, ya que WhenAll devuelve una tarea:

public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap[]> downloads =
            Task.WhenAll(from url in urls select GetBitmapAsync(url));
        if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
        {
            foreach(var bmp in downloads.Result) panel.AddImage(bmp);
            status.Text = "Downloaded";
        }
        else
        {
            status.Text = "Timed out";
            downloads.ContinueWith(t => Log(t));
        }
    }
    finally { btnDownload.Enabled = true; }
}

Construir combinadores basados en tareas

Dado que una tarea puede representar completamente una operación asincrónica y proporcionar funcionalidades sincrónicas y asincrónicas para unirse a la operación, recuperar sus resultados, etc., puede crear bibliotecas útiles de combinadores que componen tareas para crear patrones más grandes. Como se ha descrito en la sección anterior, .NET incluye varios combinadores integrados, pero también puede crear los suyos propios. En las secciones siguientes se proporcionan varios ejemplos de posibles métodos y tipos de combinadores.

RetryOnFault

En muchas situaciones, es posible que quiera volver a intentar una operación si se produce un error en un intento anterior. En el caso del código sincrónico, puede crear un método auxiliar, como RetryOnFault en el ejemplo siguiente para lograr esto:

public static T RetryOnFault<T>(
    Func<T> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return function(); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

Puede crear un método auxiliar casi idéntico para las operaciones asincrónicas que se implementan con TAP y, por tanto, devolver tareas:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

A continuación, puede usar este combinador para codificar los reintentos en la lógica de la aplicación; por ejemplo:

// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3);

Puede ampliar aún más la RetryOnFault función. Por ejemplo, la función podría aceptar otra Func<Task> que se invocará entre reintentos para determinar cuándo intentar la operación de nuevo; por ejemplo:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
        await retryWhen().ConfigureAwait(false);
    }
    return default(T);
}

Después, podría usar la función como se indica a continuación para esperar un segundo antes de volver a intentar la operación:

// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3, () => Task.Delay(1000));

NeedOnlyOne

A veces, puede aprovechar la redundancia para mejorar la latencia y las posibilidades de éxito de una operación. Tenga en cuenta varios servicios web que proporcionan cotizaciones, pero en diferentes momentos del día, cada servicio puede ofrecer diferentes niveles de calidad y tiempos de respuesta. Para tratar estas fluctuaciones, puede emitir solicitudes a todos los servicios web y, en cuanto obtenga una respuesta de una, cancele las solicitudes restantes. Puede implementar una función auxiliar para facilitar la implementación de este patrón común de inicio de varias operaciones, esperando cualquier y cancelando el resto. La NeedOnlyOne función del ejemplo siguiente ilustra este escenario:

public static async Task<T> NeedOnlyOne(
    params Func<CancellationToken,Task<T>> [] functions)
{
    var cts = new CancellationTokenSource();
    var tasks = (from function in functions
                 select function(cts.Token)).ToArray();
    var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
    cts.Cancel();
    foreach(var task in tasks)
    {
        var ignored = task.ContinueWith(
            t => Log(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return completed;
}

A continuación, puede usar esta función de la siguiente manera:

double currentPrice = await NeedOnlyOne(
    ct => GetCurrentPriceFromServer1Async("msft", ct),
    ct => GetCurrentPriceFromServer2Async("msft", ct),
    ct => GetCurrentPriceFromServer3Async("msft", ct));

Operaciones intercaladas

Hay un posible problema de rendimiento con el uso del método WhenAny para admitir un escenario de intercalación cuando se trabaja con conjuntos de tareas grandes. Todas las llamadas a WhenAny resultan en el registro de una continuación con cada tarea. Para un número N de tareas, el resultado son O(N2) continuaciones creadas durante la vigencia de la operación de intercalación. Si está trabajando con un gran conjunto de tareas, puede usar un combinador (Interleaved en el ejemplo siguiente) para solucionar el problema de rendimiento:

static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
    var inputTasks = tasks.ToList();
    var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
                   select new TaskCompletionSource<T>()).ToList();
    int nextTaskIndex = -1;
    foreach (var inputTask in inputTasks)
    {
        inputTask.ContinueWith(completed =>
        {
            var source = sources[Interlocked.Increment(ref nextTaskIndex)];
            if (completed.IsFaulted)
                source.TrySetException(completed.Exception.InnerExceptions);
            else if (completed.IsCanceled)
                source.TrySetCanceled();
            else
                source.TrySetResult(completed.Result);
        }, CancellationToken.None,
           TaskContinuationOptions.ExecuteSynchronously,
           TaskScheduler.Default);
    }
    return from source in sources
           select source.Task;
}

A continuación, puede usar el combinador para procesar los resultados de las tareas a medida que se completan; por ejemplo:

IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
    int result = await task;
    …
}

WhenAllOrFirstException

En determinados escenarios de dispersión o recopilación, es posible que desee esperar a que todas las tareas de un conjunto se completen, a menos que falle una de ellas, en cuyo caso desea dejar de esperar en cuanto se produzca la excepción. Puede hacerlo con un método combinador, como WhenAllOrFirstException en el ejemplo siguiente:

public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
    var inputs = tasks.ToList();
    var ce = new CountdownEvent(inputs.Count);
    var tcs = new TaskCompletionSource<T[]>();

    Action<Task> onCompleted = (Task completed) =>
    {
        if (completed.IsFaulted)
            tcs.TrySetException(completed.Exception.InnerExceptions);
        if (ce.Signal() && !tcs.Task.IsCompleted)
            tcs.TrySetResult(inputs.Select(t => t.Result).ToArray());
    };

    foreach (var t in inputs) t.ContinueWith(onCompleted);
    return tcs.Task;
}

Creación de estructuras de datos basadas en tareas

Además de la capacidad de crear combinadores personalizados basados en tareas, tener una estructura de datos en Task y Task<TResult> que representa tanto los resultados de una operación asincrónica como la sincronización necesaria para unirse con él hace que sea un tipo eficaz en el que crear estructuras de datos personalizadas que se usarán en escenarios asincrónicos.

AsyncCache

Un aspecto importante de una tarea es que se puede entregar a varios consumidores, todos ellos pueden esperar a que finalice, registrar las continuaciones con ella, obtener sus resultados o excepciones (en el caso de Task<TResult>), y así sucesivamente. Esto hace que Task y Task<TResult> sean perfectamente adecuados para ser utilizados en una infraestructura de almacenamiento en caché asincrónica. Este es un ejemplo de una caché asincrónica pequeña pero eficaz basada en Task<TResult>:

public class AsyncCache<TKey, TValue>
{
    private readonly Func<TKey, Task<TValue>> _valueFactory;
    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

    public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
    {
        if (valueFactory == null) throw new ArgumentNullException("valueFactory");
        _valueFactory = valueFactory;
        _map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
    }

    public Task<TValue> this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException("key");
            return _map.GetOrAdd(key, toAdd =>
                new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
        }
    }
}

La clase AsyncCache<TKey,TValue> acepta en su constructor como delegado una función que toma un TKey y devuelve un Task<TResult>. Los valores a los que se ha accedido anteriormente desde la memoria caché se almacenan en el diccionario interno y garantiza AsyncCache que solo se genere una tarea por clave, incluso si se accede a la memoria caché simultáneamente.

Por ejemplo, puede crear una memoria caché para páginas web descargadas:

private AsyncCache<string,string> m_webPages =
    new AsyncCache<string,string>(DownloadStringTaskAsync);

A continuación, puede usar esta memoria caché en métodos asincrónicos siempre que necesite el contenido de una página web. La AsyncCache clase garantiza que está descargando la menor cantidad de páginas posible y almacena en caché los resultados.

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtContents.Text = await m_webPages["https://www.microsoft.com"];
    }
    finally { btnDownload.IsEnabled = true; }
}

AsyncProducerConsumerCollection

También puede usar tareas para crear estructuras de datos para coordinar actividades asincrónicas. Considere uno de los patrones de diseño paralelo clásicos: productor/consumidor. En este patrón, los productores generan datos consumidos por los consumidores, y los productores y los consumidores pueden ejecutarlos en paralelo. Por ejemplo, el consumidor procesa el elemento 1, que anteriormente generó un productor que ahora está produciendo el artículo 2. Para el patrón de productor o consumidor, se necesita invariablemente alguna estructura de datos para almacenar el trabajo creado por los productores para que los consumidores puedan recibir notificaciones de nuevos datos y encontrarlos cuando estén disponibles.

A continuación se muestra una estructura de datos simple creada sobre las tareas, que permite el uso de métodos asincrónicos como productores y consumidores:

public class AsyncProducerConsumerCollection<T>
{
    private readonly Queue<T> m_collection = new Queue<T>();
    private readonly Queue<TaskCompletionSource<T>> m_waiting =
        new Queue<TaskCompletionSource<T>>();

    public void Add(T item)
    {
        TaskCompletionSource<T> tcs = null;
        lock (m_collection)
        {
            if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
            else m_collection.Enqueue(item);
        }
        if (tcs != null) tcs.TrySetResult(item);
    }

    public Task<T> Take()
    {
        lock (m_collection)
        {
            if (m_collection.Count > 0)
            {
                return Task.FromResult(m_collection.Dequeue());
            }
            else
            {
                var tcs = new TaskCompletionSource<T>();
                m_waiting.Enqueue(tcs);
                return tcs.Task;
            }
        }
    }
}

Con esa estructura de datos en su lugar, puede escribir código como el siguiente:

private static AsyncProducerConsumerCollection<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.Take();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Add(data);
}

El System.Threading.Tasks.Dataflow espacio de nombres incluye el BufferBlock<T> tipo , que puede usar de forma similar, pero sin tener que crear un tipo de colección personalizado:

private static BufferBlock<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.ReceiveAsync();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Post(data);
}

Nota:

El System.Threading.Tasks.Dataflow espacio de nombres está disponible como un paquete NuGet. Para instalar el ensamblado que contiene el espacio de nombres System.Threading.Tasks.Dataflow, abra su proyecto en Visual Studio, elija Administrar paquetes NuGet en el menú Proyecto y busque en línea el paquete System.Threading.Tasks.Dataflow.

Consulte también