Consumindo o padrão assíncrono baseado em tarefa

Quando você usa o TAP (padrão assíncrono baseado em tarefa) para trabalhar com operações assíncronas, você pode usar retornos de chamada para obter uma espera sem bloqueio. Para tarefas, isso é conseguido por meio de métodos como Task.ContinueWith. O suporte assíncrono baseado em linguagem oculta retornos de chamada ao permitir que operações assíncronas sejam colocadas em espera no fluxo de controle normal, sendo que o código gerado pelo compilador fornece esse mesmo suporte de nível de API.

Suspendendo a execução com Await

Você pode usar a palavra-chave await em C# e o Await Operator no Visual Basic para aguardar objetos Task e Task<TResult> de forma assíncrona. Quando você está aguardando um Task, a expressão await é do tipo void. Quando você está aguardando um Task<TResult>, a expressão await é do tipo TResult. Uma expressão await deve ocorrer dentro do corpo de um método assíncrono. (Esses recursos de linguagem foram introduzidos no .NET Framework 4.5.)

Nos bastidores, a funcionalidade await instala um retorno de chamada na tarefa pelo uso de uma continuação. Esse retorno de chamada retoma o método assíncrono no ponto de suspensão. Quando o método assíncrono é retomado, se a operação de espera foi concluída com êxito e era uma Task<TResult>, o respectivo TResult é retornado. Se o Task ou Task<TResult> que foi aguardado terminou no estado Canceled, uma exceção OperationCanceledException será lançada. Se o Task ou Task<TResult> que foi aguardado terminou no estado Faulted, uma exceção que causou a falha será lançada. Uma Task pode falhar como resultado de várias exceções, mas apenas uma dessas exceções é propagada. No entanto, a propriedade Task.Exception retorna uma exceção AggregateException que contém todos os erros.

Se um contexto de sincronização (objeto SynchronizationContext) é associado ao thread que estava executando o método assíncrono no momento da suspensão (por exemplo, se a propriedade SynchronizationContext.Current não é null), o método assíncrono é retomado no mesmo contexto de sincronização usando o método Post do contexto. Caso contrário, ele utiliza o Agendador de Tarefas (objeto TaskScheduler) que era atual no momento da suspensão. Normalmente, esse é que o agendador de tarefas padrão (TaskScheduler.Default), que tem como alvo o pool de threads. O Agendador de Tarefas determina se a operação assíncrona aguardada deve continuar do ponto em que foi concluída ou se a retomada deve ser agendada. O agendador padrão geralmente permite que a continuação seja executada no thread em que a operação aguardada foi concluída.

Quando um método assíncrono é chamado, ele executa o corpo da função de modo síncrono até a primeira expressão await em uma instância aguardável que ainda não tenha foi concluída, no ponto em que a invocação retorna ao chamador. Se o método assíncrono não retornar void, um objeto Task ou Task<TResult> será retornado para representar o cálculo em andamento. Em um método assíncrono não nulo, se uma instrução return for encontrada ou o final do corpo do método for atingido, a tarefa será concluída no estado final RanToCompletion. Se uma exceção sem tratamento fizer com que o controle deixe o corpo do método assíncrono, a tarefa terminará no estado Faulted. Se essa exceção for um OperationCanceledException, a tarefa terminará no estado Canceled. Dessa maneira, a exceção ou o resultado é eventualmente publicado.

Há diversas variações importantes desse comportamento. Por motivos de desempenho, se uma tarefa já foi concluída até o momento em que a tarefa é aguardada, o controle não é suspenso e a função continua a executar. Além disso, retornar para o contexto original nem sempre é o comportamento desejado e pode ser alterado; isso é descrito mais detalhadamente na próxima seção.

Configurando a suspensão e retomada com Yield e ConfigureAwait

Vários métodos fornecem mais controle sobre a execução de um método assíncrono. Por exemplo, você pode usar o método Task.Yield para introduzir um ponto de rendimento para o método assíncrono:

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

Isso é equivalente a postar assincronamente ou agendar de volta para o contexto atual.

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

Você também pode usar o método Task.ConfigureAwait para controlar melhor a suspensão e a retomada de um método assíncrono. Conforme mencionado anteriormente, por padrão, o contexto atual é capturado no momento em que um método assíncrono é suspenso e esse contexto capturado é usado para invocar a continuação do método assíncrono após a retomada. Em muitos casos, esse é o comportamento exato desejado. Em outros casos, você pode não se importar com o contexto de continuação e pode obter o melhor desempenho ao evitar essas postagens de volta para o contexto original. Para habilitar isso, use o método Task.ConfigureAwait para informar à operação de espera que não capture e retome no contexto, mas sim continue a execução no ponto em que a operação assíncrona que estava sendo aguardada foi concluída, seja qual for esse ponto:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Cancelando uma operação assíncrona

A partir de NET Framework 4, os métodos TAP que dão suporte ao cancelamento fornecem pelo menos uma sobrecarga que aceita um token de cancelamento (objeto CancellationToken).

Um token de cancelamento é criado por meio de uma origem de token de cancelamento (objeto CancellationTokenSource). A propriedade Token da fonte retorna o token de cancelamento que sinalizará quando o método Cancel da fonte será chamado. Por exemplo, se você desejar baixar uma única página da Web e você deseja ser capaz de cancelar a operação, crie um objeto CancellationTokenSource, passe o token desse objeto para o método TAP e, em seguida, chame o método Cancel da origem quando estiver pronto para cancelar a operação:

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

Para cancelar várias invocações assíncronas, você pode passar o mesmo token para todas as invocações:

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();

Ou então, você pode passar o mesmo token para um subconjunto seletivo de operações:

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

Solicitações de cancelamento podem ser iniciadas de qualquer thread.

Você pode passar o valor CancellationToken.None a qualquer método que aceite um token de cancelamento para indicar que o cancelamento nunca será solicitado. Isso faz a propriedade CancellationToken.CanBeCanceled retornar false, e o método chamado pode ser otimizado adequadamente. Para fins de teste, você também pode passar um token de cancelamento previamente cancelado que é instanciado pelo uso do construtor que aceita um valor booliano para indicar se o token deve iniciar em um estado não cancelável ou já cancelado.

Essa abordagem para o cancelamento tem várias vantagens:

  • Você pode passar o mesmo token de cancelamento para qualquer número de operações síncronas e assíncronas.

  • A mesma solicitação de cancelamento pode ser proliferada para qualquer número de ouvintes.

  • O desenvolvedor da API assíncrona tem total controle com relação ao cancelamento poder ou não ser solicitado e a quando isso pode entrar em vigor.

  • O código que consome a API pode, seletivamente, determinar as invocações assíncronas para as quais as solicitações de cancelamento serão propagadas.

Monitorando o progresso

Alguns métodos assíncronos expõem progresso através de uma interface de progresso passada para o método assíncrono. Por exemplo, considere uma função que baixa de forma assíncrona uma cadeia de caracteres de texto e, ao longo do caminho, gera atualizações de progresso que incluem a porcentagem do download concluído até o momento. Tal método poderia ser consumido em um aplicativo do WPF (Windows Presentation Foundation) da seguinte maneira:

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

Usando os combinadores baseados em tarefas internos

O namespace System.Threading.Tasks inclui vários métodos para compor tarefas e trabalhar com elas.

Task.Run

A classe Task inclui vários métodos Run que permitem descarregar com facilidade o trabalho como um Task ou Task<TResult> para o pool de threads, por exemplo:

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

Alguns desses métodos Run, como a sobrecarga de Task.Run(Func<Task>), existe como um atalho para o método TaskFactory.StartNew. Esta sobrecarga permite que você use await dentro do trabalho descarregado, por exemplo:

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

Essas sobrecargas são logicamente equivalentes a usar o método TaskFactory.StartNew junto com o método de extensão Unwrap na biblioteca de paralelismo de tarefas.

Task.FromResult

Use o método FromResult em cenários nos quais os dados talvez já estejam disponíveis e apenas precisem ser retornados de um método de retorno de tarefas elevado para uma 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

Use o método WhenAll para aguardar de forma assíncrona várias operações assíncronas que são representadas como tarefas. O método tem várias sobrecargas que dão suporte a um conjunto de tarefas não genéricas ou um conjunto não uniforme de tarefas genéricas (por exemplo, aguardar de forma assíncrona várias operações que retornam nulo ou aguardar vários métodos que retornam um valor em que cada valor pode ter um tipo diferente) e dão suporte a um conjunto uniforme de tarefas genéricas (como aguardar de forma assíncrona vários métodos que retornam TResult).

Digamos que você deseja enviar mensagens de email para vários clientes. Você pode sobrepor o envio de mensagens de modo que você não aguarde a conclusão de uma mensagem antes de enviar a próxima. Você também pode descobrir quando as operações de envio foram concluídas e se ocorreu algum erro:

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

Esse código não tratará explicitamente exceções que possam ocorrer, mas permitirá que as exceções sejam propagadas para fora do await na tarefa resultante de WhenAll. Para tratar as exceções, você pode usar código como o seguinte:

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

Nesse caso, se alguma operação assíncrona falhar, todas as exceções serão consolidadas em uma exceção AggregateException, que é armazenada no Task que é retornado do método WhenAll. No entanto, apenas uma dessas exceções é propagada pela palavra-chave await. Se você quiser examinar todas as exceções, poderá reescrever o código anterior da seguinte maneira:

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

Vamos considerar um exemplo em que baixamos vários arquivos da Web de forma assíncrona. Nesse caso, todas as operações assíncronas têm tipos de resultado homogêneos e é fácil acessar os resultados:

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

Você pode usar as mesmas técnicas de tratamento de exceções discutidas no cenário anterior, em que nulo é retornado:

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

Você pode usar o método WhenAny para aguardar de forma assíncrona apenas uma de várias operações assíncronas que são representadas como tarefas. Esse método tem quatro casos de uso principais:

  • Redundância: executar uma operação várias vezes e selecionar aquela que é concluída primeiro (por exemplo, entrando em contato com vários serviços Web de cota de ações que produzirão um único resultado e selecionar aquele que termina mais rápido).

  • Intercalação: iniciar várias operações e aguardar que todas eles sejam concluídas, mas processando-as conforme são concluídas.

  • Limitação: permitir que operações adicionais comecem conforme outras forem concluídas. Essa é uma extensão do cenário de intercalação.

  • Saída precoce: por exemplo, uma operação representada pela tarefa t1 pode ser agrupada em uma tarefa WhenAny com outra tarefa t2 e você pode aguardar na tarefa WhenAny. A tarefa t2 pode representar um tempo limite, um cancelamento ou algum outro sinal que faça com que a tarefa WhenAny seja concluída antes de t1.

Redundância

Considere um caso em que você deseja tomar uma decisão sobre a possibilidade de comprar uma ação. Há diversos serviços Web de recomendação de compra de ações nos quais você confia, mas dependendo da carga diária, cada serviço pode acabar sendo lento em horários diferentes. Você pode usar o método WhenAny para receber uma notificação quando qualquer operação for concluída:

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

Ao contrário de WhenAll, que retorna os resultados não encapsulados de todas as tarefas concluídas com êxito, o WhenAny retorna a tarefa que foi concluída. Se uma tarefa falhar, será importante saber que ela falhou e, se uma tarefa for bem-sucedida, será importante saber a qual tarefa o valor retornado é associado. Portanto, você precisa acessar o resultado da tarefa retornada ou aguardá-la ainda mais, conforme mostra este exemplo.

Assim como acontece com o WhenAll, você precisa ser capaz de acomodar as exceções. Devido a você receber novamente a tarefa concluída, você poderá esperar a tarefa retornada para então propagar os erros e aplicar try/catch a eles adequadamente; por exemplo:

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

Além disso, mesmo que uma primeira tarefa seja concluída com êxito, as tarefas subsequentes poderão falhar. Neste ponto, você tem várias opções para lidar com exceções: você pode esperar até que todas as tarefas iniciadas tenham sido concluídas, caso em que você pode usar o método WhenAll, ou você pode decidir que todas as exceções são importantes e devem estar conectadas. Para isso, você pode usar as continuações para receber uma notificação quando tarefas foram concluídas de forma assíncrona:

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

ou:

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

ou até mesmo:

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

Finalmente, você talvez queira cancelar todas as operações 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);

Intercalação

Considere um caso em que você está baixando imagens da Web e processando cada imagem (por exemplo, adicionando a imagem a um controle de interface do usuário). Você processa as imagens sequencialmente no thread da interface do usuário, mas deseja fazer download das imagens o mais simultaneamente possível. Além disso, você não quer adiar a adição das imagens à interface do usuário até que todas sejam baixadas. Em vez disso, você quer adicioná-las conforme forem concluídas.

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

Você também pode aplicar interpolação para um cenário que envolve o processamento de recursos computacionais no ThreadPool das imagens baixadas; por exemplo:

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

Limitação

Considere o exemplo de intercalação, exceto que o usuário está baixando um número tão grande de imagens que os downloads precisam ser limitados; por exemplo, você deseja que apenas um número específico de downloads ocorram simultaneamente. Para conseguir isso, você pode iniciar um subconjunto das operações assíncronas. Conforme as operações forem concluídas, você pode iniciar operações adicionais para assumir o lugar delas:

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

Saída precoce

Considere a possibilidade de que você está aguardando de forma assíncrona que uma operação seja concluída e, simultaneamente, respondendo a uma solicitação de cancelamento do usuário (por exemplo, o usuário clicou em um botão para cancelar). O código a seguir ilustra esse cenário:

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

Essa implementação reabilita a interface do usuário assim que você decide sair, mas não as cancela as operações assíncronas subjacentes. Outra alternativa seria cancelar as operações pendentes quando você decide sair, mas não restabelecer a interface do usuário até que as operações sejam concluídas, possivelmente devido a um encerramento precoce devido à solicitação de cancelamento:

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

Outro exemplo de saída precoce envolve o uso do método WhenAny junto com o método Delay, conforme discutido na próxima seção.

Task.Delay

Você pode usar o método Task.Delay para apresentar pausas em uma execução de um método assíncrono. Isso é útil para muitos tipos de funcionalidades, incluindo o build de loops de sondagem e o atraso da manipulação da entrada do usuário por um período de tempo predeterminado. O método Task.Delay também pode ser útil em combinação com o Task.WhenAny para implementação de tempos limite em esperas.

Se uma tarefa que for parte de uma operação assíncrona maior (por exemplo, um serviço Web ASP.NET) levar muito tempo para concluir, a operação geral poderá ser afetada, especialmente se ela falhar e nunca for concluída. Por esse motivo, é importante ser capaz de atingir o tempo limite ao aguardar uma operação assíncrona. Os métodos Task.Wait, Task.WaitAll e Task.WaitAny aceitam valores de tempo limite, mas os correspondentes TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny e os métodos Task.WhenAll/Task.WhenAny mencionados anteriormente não. Em vez disso, você pode usar Task.Delay e Task.WhenAny em conjunto para implementar um tempo limite.

Por exemplo, em seu aplicativo de interface do usuário, digamos que você deseja baixar uma imagem e desabilitar a interface do usuário durante esse download. No entanto, se o download levar muito tempo, você desejará reabilitar a interface do usuário e descartar o download:

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

O mesmo aplica-se a vários downloads, porque WhenAll retorna uma tarefa:

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

Compilando combinadores baseados em tarefa

Já que uma tarefa é capaz de representar totalmente uma operação assíncrona e fornecer recursos síncronos e assíncronos para ingressar na operação, recuperar seus resultados e assim por diante, você pode compilar bibliotecas úteis de combinadores que compõem tarefas para compilar padrões maiores. Conforme discutido na seção anterior, .NET inclui vários combinadores internos, mas você também pode compilar os seus próprios combinadores. As seções a seguir fornecem vários exemplos de tipos e métodos combinadores potenciais.

RetryOnFault

Em muitas situações, talvez você queira repetir uma operação se uma tentativa anterior falhar. Para código síncrono, você pode compilar um método auxiliar como RetryOnFault no exemplo a seguir para fazer isso:

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

Você pode compilar um método auxiliar quase idêntico para operações assíncronas implementadas com TAP e, assim, retornar tarefas:

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

Você pode usar este combinador para codificar repetições na lógica do aplicativo, por exemplo:

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

Você pode estender ainda mais a função RetryOnFault. Por exemplo, a função poderá aceitar outro Func<Task> que será invocado entre as tentativas para determinar quando tentar novamente a operação, por exemplo:

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

Você pode então usar a função da seguinte maneira, para aguardar um segundo antes de repetir a operação:

// 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

Às vezes, você pode tirar proveito da redundância para melhorar a latência e as chances de sucesso de uma operação. Considere vários serviços Web que fornecem cotações de ações, mas em diferentes horas do dia, cada serviço pode fornecer diferentes níveis de qualidade e tempos de resposta. Para lidar com essas flutuações, você poderá emitir solicitações para todos os serviços Web e, assim que você obtiver uma resposta de um deles, cancelar as solicitações restantes. Você pode implementar uma função auxiliar para facilitar a implementação desse padrão comum de inicialização de várias operações, aguardar uma delas e, em seguida, cancelar o restante. A função NeedOnlyOne no exemplo a seguir ilustra esse cenário:

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

Você pode então usar essa função conforme demonstrado a seguir:

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

Operações intercaladas

Há um possível problema de desempenho ao usar o método WhenAny para oferecer suporte a um cenário de intercalação ao trabalhar com grandes conjuntos de tarefas. Todas as chamadas para WhenAny fazem uma continuação ser registrada com cada tarefa. Para um número N de tarefas, isso resulta em O(N2) continuações criadas durante o tempo de vida da operação de intercalação. Se você estiver trabalhando com um grande conjunto de tarefas, você poderá usar um combinador (Interleaved no exemplo a seguir) para solucionar o problema de desempenho:

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

Você poderá usar o combinador para processar os resultados das tarefas conforme elas forem concluídas, por exemplo:

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

WhenAllOrFirstException

Em determinados cenários de dispersão/coleta, convém esperar por todas as tarefas em um conjunto, a menos que uma delas falhe, caso em que você desejará interromper a espera assim que a exceção ocorrer. Você pode realizar isso com um método combinador tal como WhenAllOrFirstException no exemplo a seguir:

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

Compilando estruturas de dados com base em tarefa

Além da capacidade de compilar combinadores baseados em tarefas personalizados, ter uma estrutura de dados em Task e Task<TResult>, que representa tanto os resultados de uma operação assíncrona quanto a sincronização necessária para unir a ela, torna esse um tipo poderoso no qual compilar estruturas de dados personalizadas a serem usados em cenários assíncronos.

AsyncCache

Um aspecto importante de uma tarefa é que ela pode ser enviada para vários consumidores, todos os quais podem esperar por ela, registrar continuações com ela, obter seu resultado ou exceções (no caso de Task<TResult>) e assim por diante. Isso torna Task e Task<TResult> perfeitamente adequado para ser usado em uma infraestrutura de cache assíncrona. Aqui está um exemplo de um cache assíncrono pequeno mas poderoso, construído com base na 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;
        }
    }
}

A classe AsyncCache<TKey,TValue> aceita como um delegado para seu construtor uma função que recebe um TKey e retorna um Task<TResult>. Quaisquer valores do cache previamente acessados são armazenados no dicionário interno e AsyncCache garante que apenas uma tarefa seja gerada por chave, mesmo que o cache seja acessado simultaneamente.

Por exemplo, você pode compilar um cache para páginas da Web baixadas:

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

Você pode em seguida usar esse cache em métodos assíncronos sempre que precisar do conteúdo de uma página da Web. A classe AsyncCache garante que você esteja baixando o mínimo possível de páginas e armazena os resultados em cache.

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

Você também pode usar tarefas para compilar estruturas de dados para coordenar atividades assíncronas. Considere um dos padrões de design paralelos clássicos: produtor/consumidor. Nesse padrão, produtores geram dados que são consumidos pelos consumidores e produtores e consumidores podem executar em paralelo. Por exemplo, o consumidor processa o item 1, que anteriormente foi gerado por um produtor que agora está produzindo o item 2. Para o padrão de produtor/consumidor, você precisa invariavelmente que alguma estrutura de dados armazene o trabalho criado pelos produtores para que os consumidores possam ser notificados sobre novos dados e localizá-los quando disponíveis.

Eis aqui uma estrutura de dados simples criada sobre tarefas que permitem que os métodos assíncronos sejam usados como produtores e 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;
            }
        }
    }
}

Com essa estrutura de dados estabelecida, você pode escrever código como o seguinte:

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

O namespace System.Threading.Tasks.Dataflow inclui o tipo BufferBlock<T>, que pode ser usado de maneira semelhante, mas sem a necessidade de criar um tipo de coleção 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);
}

Observação

O namespace System.Threading.Tasks.Dataflow está disponível como um pacote NuGet. Para instalar o assembly que contém o namespace System.Threading.Tasks.Dataflow, abra seu projeto no Visual Studio, escolha Gerenciar Pacotes NuGet no menu Projeto e procure online pelo pacote System.Threading.Tasks.Dataflow.

Confira também