Utilizar el modelo asincrónico basado en tareas

Cuando se utiliza el modelo asincrónico basado en tareas (TAP) para trabajar con operaciones asincrónicas, puede utilizar las devoluciones de llamada para conseguir esperas sin bloqueos. Para las tareas, esto se consigue con métodos como Task.ContinueWith. La compatibilidad asincrónica basada en lenguaje oculta las devoluciones de llamada al permitir que las operaciones asincrónicas se puedan esperar en el flujo de control normal y que el código generado por el compilador proporcione esta misma compatibilidad de nivel de API.

Suspender la ejecución con Await

Puede usar la palabra clave await de C# y el operador Await de Visual Basic para esperar de forma asincrónica los objetos Task y Task<TResult>. 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 del lenguaje se presentaron 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 propiedad Task.Exception devuelve una excepción AggregateException 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 era actual en el momento de la suspensión. Normalmente, se trata del programador de tareas predeterminado (TaskScheduler.Default), que tiene como destino el grupo de subprocesos. El programador de tareas determina si la operación asincrónica en espera debe reanudarse donde ha completado o si se debe programar la reanudación. Normalmente, el programador predeterminado permite que la continuación se ejecute en el subproceso que ha completado la operación de 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 objeto Task 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 abandone el cuerpo del método asincrónico, la tarea finaliza en el estado Faulted. Si esa excepción es una clase OperationCanceledException, en su lugar, la tarea finaliza con el estado Canceled. De esta manera, el resultado o excepción se publica finalmente.

Hay diversas variaciones importantes de este comportamiento. Por motivos de rendimiento, si ya ha completado una tarea en el momento en que se esperaba, no se obtiene 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.

Configurar 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 método Task.Yield 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 método Task.ConfigureAwait para controlar mejor la suspensión y reanudación de un método asincrónico. Como se mencionó anteriormente, el contexto actual se captura de forma predeterminada en el momento en que se suspende un método asincrónico, y ese contexto capturado se utiliza 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);

Cancelar una operación asincrónica

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

Se crea un token de cancelación a través de 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 de origen. Por ejemplo, si desea descargar una única página web y desea poder cancelar la operación, cree un objeto CancellationTokenSource, pase su token para el método TAP y luego llame al método Cancel de 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 para todas las llamadas:

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 de operaciones selectivo:

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 subproceso.

Puede pasar el valor CancellationToken.None a cualquier método que acepta un token de cancelación para indicar que nunca se le solicitó la cancelación. Esto hace que la propiedad CancellationToken.CanBeCanceled devuelva false, y el método llamado puede optimizarse 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 de cancelación tiene varias ventajas:

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

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

  • El desarrollador de la API asincrónica tiene todo el control de si se puede solicitar la cancelación y cuándo puede surtir efecto.

  • El código que utiliza la API puede determinar de forma selectiva las llamadas asincrónicas que se propagarán las solicitudes de cancelación.

Supervisar el progreso

Algunos métodos asincrónicos exponen progreso a través de una interfaz de progreso pasada al método asincrónico. Por ejemplo, plantéese usar una función que descargue de manera asincrónica una cadena de texto y que genere actualizaciones de progreso que incluyan el porcentaje de descarga que se ha completado hasta el momento. Este método se puede utilizar en una aplicación de Windows Presentation Foundation (WPF) como sigue:

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; }
}

Usar 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 le permiten descargar trabajo con facilidad como las clases 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);
    });
}

Tales sobrecargas, lógicamente, son equivalentes al uso del método TaskFactory.StartNew junto con el método de extensión Unwrap en la biblioteca TPL.

Task.FromResult

Utilice el método FromResult en escenarios donde los datos estén disponibles y solo debe devolverse desde un método de devolución de tarea de elevación en un método 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, esperando de forma asincrónica varias operaciones que devuelven void o esperando de forma asincrónica varios métodos que devuelven valores, donde cada valor puede tener un tipo diferente) y para admitir un conjunto uniforme de tareas genéricas (como esperar de forma asincrónica para varios métodos que devuelven TResult).

Supongamos que desea enviar mensajes de correo electrónico a varios clientes. Puede superponer el envío de los mensajes, para que no esté esperando que se complete un mensaje antes de enviar el siguiente. También puede averiguar cuando se completan las operaciones de envío y si se ha producido algún error:

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 aparecer, pero permite que las excepciones se propaguen fuera de await en la tarea resultante de WhenAll. Para controlar las excepciones, puede utilizar 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 cualquier operación asincrónica, todas las excepciones se consolidarán en una excepción AggregateException, que se almacena en la clase Task devuelta por el método WhenAll. Sin embargo, solo una de esas excepciones se propaga por la palabra clave await. Si desea examinar todas las excepciones, puede volver a escribir el código anterior como sigue:

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 descarga de varios archivos desde la web de forma asincrónica. En este caso, todas las operaciones asincrónicas tienen tipos de resultado 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 método WhenAny para esperar de manera asincrónica solo una de varias operaciones asincrónicas que se representan como tareas para completar. Este método actúa en cuatro casos de uso principales:

  • Redundancia: realizar una operación varias veces y seleccionar la que se complete primero (por ejemplo, ponerse en contacto con varios servicios web de cotización bursátil que va a generar un único resultado y seleccionar la que se completa con más rapidez).

  • 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 cancelación, o alguna otra señal que hace que la tarea WhenAny se complete antes de que finalice t1.

Redundancia

Considere un caso en el que quiera tomar la decisión de comprar o no una acción. Hay varios servicios web de recomendación bursátil en los que confía, pero según la carga diaria, cada servicio puede acabar ralentizándose en momentos diferentes. Puede usar el método WhenAny 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 de WhenAll, que devuelve los resultados desajustados de todas las tareas que se han completado correctamente, WhenAny devuelve la tarea completada. 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 se asocia el valor devuelto. Por lo tanto, necesitará acceder al resultado de la tarea devuelta o esperarla aún más, tal como se muestra en este ejemplo.

Al igual que con WhenAll, tendrá que ser capaz de alojar las 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, aunque una primera tarea se complete correctamente, las tareas subsiguientes pueden producir un error. En este punto, tiene varias opciones para tratar las excepciones: puede esperar hasta que han completado todas las tareas iniciadas, en cuyo caso puede utilizar el método WhenAll, o bien puede decidir que todas las excepciones son importantes y se deben registrar. Para ello, puede usar las continuaciones para recibir una notificación cuando se hayan completado las tareas de forma asincrónica:

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

O bien

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 querer 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 donde descarga imágenes de la web y procesa cada imagen (por ejemplo, agregando 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 mantener la adición de las imágenes en la interfaz de usuario hasta que todas se hayan descargado. 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{}
}

Limitación de peticiones

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 lograr esto, puede iniciar un subconjunto de operaciones asincrónicas. Cuando se completen las operaciones, puede iniciar operaciones adicionales que ocupen 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

Suponga que espera asincrónicamente a que se complete una operación mientras se responde simultáneamente a la solicitud de cancelación de un usuario (por ejemplo, el usuario hace clic en un botón Cancelar). El código siguiente 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 recursividad temprana conlleva usar el método WhenAny junto con el método Delay, como se describe en la sección siguiente.

Task.Delay

Puede usar el método Task.Delay para introducir pausas en la ejecución de un método asincrónico. Esto es útil para muchos tipos de funcionalidad, incluida la generación de bucles de sondeo y el retraso del control de entrada de usuario durante un período 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 ASP.NET) tarda demasiado en completarse, la operación global podría verse afectada, especialmente si no se puede completar. Por este motivo, es importante poder agotar el tiempo de espera al esperar a una operación asincrónica. Los métodos Task.Wait, Task.WaitAll y Task.WaitAny sincrónicos aceptan valores de tiempo de espera, pero TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny y los métodos Task.WhenAll/Task.WhenAny mencionados anteriormente no los aceptan. En su lugar, puede usar Task.Delay y Task.WhenAny en combinación con la implementación de un tiempo de espera.

Por ejemplo, en la aplicación de la 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 mucho 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, porque 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; }
}

Crear combinadores basados en tareas

Como una tarea se puede representar completamente una operación asincrónica y proporciona funcionalidades sincrónicas y asincrónicas para combinarse con la operación y recuperar sus resultados, entre otros, 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 otros propios. Las secciones siguientes proporcionan varios ejemplos de tipos y métodos de combinadores posibles.

RetryOnFault

En muchas situaciones, puede querer reintentar la operación si se produce un error en un intento anterior. Para el código sincrónico, podría crear un método del asistente 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 del asistente prácticamente idéntico para las operaciones asincrónicas que se implementan con TAP y, por tanto, devolver las 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);
}

Después 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 función RetryOnFault. Por ejemplo, la función puede aceptar otro Func<Task> que se invocará entre reintentos para determinar cuándo volver a intentar la operación, 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, puede utilizar la función siguiente para esperar un segundo antes de reintentar 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 sacar partido de la redundancia para mejorar la latencia y las posibilidades de éxito de una operación. Considere la posibilidad de varios servicios web que proporcionan cotizaciones bursátiles, pero a lo largo del día cada servicio puede proporcionar diferentes niveles de calidad y tiempos de respuesta. Para tratar estas fluctuaciones, pueden emitir solicitudes a todos los servicios web y tan pronto como obtiene una respuesta de uno, cancelar las solicitudes restantes. Puede implementar una función del asistente para que resulte más fácil de implementar este patrón común de iniciar varias operaciones, esperar alguna y, después, cancelar el resto. La función NeedOnlyOne del ejemplo siguiente muestra 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;
}

Después puede utilizar esta función como sigue:

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 trabaja con un conjunto de tareas grande, 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;
}

Después 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 ciertos escenarios de dispersión y recopilación, puede querer esperar a todas las tareas de un conjunto, a menos que una de ellas falle, en cuyo caso deseará detener la espera en cuanto se produzca la excepción. Puede conseguirlo con un método de 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;
}

Crear estructuras de datos basadas en tareas

Además de la capacidad de generar combinadores basados en tareas personalizadas, disponer de una estructura de datos de Task y Task<TResult> que representa tanto los resultados de una operación asincrónica como la sincronización necesaria para unirse a ella la convierte en un tipo eficaz para crear estructuras de datos personalizadas para usarse 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 perfectos para usarse en una infraestructura de almacenamiento en caché asincrónica. Este es un ejemplo de una caché asincrónica pequeña pero muy eficaz creada a partir del objeto 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 como un delegado a su constructor una función que adopta TKey y devuelve una clase Task<TResult>. Todos los valores a los que se accedió previamente desde la memoria caché se almacenan en el diccionario interno y AsyncCache asegura que solo una tarea se genera por clave, incluso si se tiene acceso a la memoria caché al mismo tiempo.

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

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

Después puede utilizar esta caché en métodos asincrónicos siempre que necesite el contenido de una página web. La clase AsyncCache garantiza que está descargando el menor número 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 la coordinación de actividades asincrónicas. Considere uno de los patrones de diseño paralelo clásico: 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 fue generado previamente por un productor que ahora está produciendo el elemento 2. Para el modelo productor-consumidor, necesita invariablemente alguna estructura de datos para almacenar los trabajos creados por los productores, para que los consumidores puedan recibir notificaciones de nuevos datos y encontrarlos si 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 esta 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 espacio de nombres System.Threading.Tasks.Dataflow incluye el tipo BufferBlock<T>, que se 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 espacio de nombres System.Threading.Tasks.Dataflow está disponible como paquete NuGet. Para instalar el ensamblado que contiene el espacio de nombres System.Threading.Tasks.Dataflow, abra el proyecto en Visual Studio, elija Administrar paquetes NuGet en el menú Proyecto y busque en línea el paquete System.Threading.Tasks.Dataflow.

Vea también