Share via


Consumindo o padrão assíncrono baseado em tarefas

Ao usar o padrão assíncrono baseado em tarefas (TAP) para trabalhar com operações assíncronas, você pode usar retornos de chamada para obter espera sem bloquear. Para as tarefas, isto é conseguido através de métodos como Task.ContinueWith. O suporte assíncrono baseado em linguagem oculta retornos de chamada, permitindo que operações assíncronas sejam aguardadas dentro do fluxo de controle normal, e o código gerado pelo compilador fornece esse mesmo suporte em nível de API.

Suspendendo a execução com o Await

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

Sob as capas, a funcionalidade await instala um retorno de chamada na tarefa usando uma continuação. Este 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 aguardada foi concluída com êxito e foi um Task<TResult>, ele TResult é retornado. Se o Task ou Task<TResult> o que era esperado terminou no Canceled estado, uma OperationCanceledException exceção é lançada. Se o Task ou Task<TResult> que era aguardado Faulted terminou no estado, a exceção que causou a culpa é lançada. Uma Task falha pode como resultado de várias exceções, mas apenas uma dessas exceções é propagada. No entanto, a Task.Exception propriedade retorna uma AggregateException exceção que contém todos os erros.

Se um contexto de sincronização (SynchronizationContext objeto) estiver associado ao thread que estava executando o método assíncrono no momento da suspensão (por exemplo, se a SynchronizationContext.Current propriedade não nullfor ), o método assíncrono será retomado nesse mesmo contexto de sincronização usando o método do Post contexto. Caso contrário, ele depende do agendador de tarefas (TaskScheduler objeto) que estava atualizado no momento da suspensão. Normalmente, esse é o agendador de tarefas padrão (TaskScheduler.Default), que tem como alvo o pool de threads. Este agendador de tarefas determina se a operação assíncrona esperada deve ser retomada onde foi concluída ou se a retomada deve ser agendada. O agendador padrão normalmente permite que a continuação seja executada no thread que a operação aguardada foi concluída.

Quando um método assíncrono é chamado, ele executa de forma síncrona o corpo da função até a primeira expressão await em uma instância aguardada que ainda não foi concluída, momento em que a invocação retorna ao chamador. Se o método assíncrono não retornar void, um Task ou Task<TResult> objeto será retornado para representar a computação em andamento. Em um método assíncrono RanToCompletion não vazio, 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. Se uma exceção não tratada fizer com que o controle deixe o corpo do método assíncrono, a tarefa terminará no Faulted estado. Se essa exceção for um OperationCanceledException, a tarefa termina no Canceled estado. Desta forma, o resultado ou exceção é eventualmente publicado.

Existem várias variações importantes deste comportamento. Por motivos de desempenho, se uma tarefa já tiver sido concluída no momento em que a tarefa é aguardada, o controle não será gerado e a função continuará a ser executada. Além disso, retornar ao contexto original nem sempre é o comportamento desejado e pode ser alterado; Isso é descrito com mais detalhes na próxima seção.

Configurando a suspensão e a 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 Task.Yield método para introduzir um ponto de rendimento no método assíncrono:

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

Isso equivale a lançar ou agendar de forma assíncrona 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 para um melhor controle sobre a Task.ConfigureAwait suspensão e a retomada em um método assíncrono. Como 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, este é o comportamento exato que você quer. Em outros casos, você pode não se importar com o contexto de continuação e pode obter um melhor desempenho evitando que essas postagens voltem ao contexto original. Para habilitar isso, use o Task.ConfigureAwait método para informar a operação await não para capturar e retomar no contexto, mas para continuar a execução sempre que a operação assíncrona que estava sendo aguardada for concluída:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Cancelando uma operação assíncrona

A partir do .NET Framework 4, os métodos TAP que suportam cancelamento fornecem pelo menos uma sobrecarga que aceita um token de cancelamento (CancellationToken objeto).

Um token de cancelamento é criado através de uma fonte de token de cancelamento (CancellationTokenSource objeto). A propriedade source's Token retorna o token de cancelamento que será sinalizado quando o método source's Cancel for chamado. Por exemplo, se você quiser baixar uma única página da Web e quiser poder cancelar a operação, crie um CancellationTokenSource objeto, passe seu token para o método TAP e chame o método da Cancel fonte 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, 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

Os pedidos de cancelamento podem ser iniciados a partir de qualquer thread.

Você pode passar o CancellationToken.None valor para qualquer método que aceite um token de cancelamento para indicar que o cancelamento nunca será solicitado. Isso faz com que a CancellationToken.CanBeCanceled propriedade retorne falsee o método chamado pode otimizar de acordo. Para fins de teste, você também pode passar um token de cancelamento pré-cancelado que é instanciado usando o construtor que aceita um valor Boolean para indicar se o token deve começar em um estado já cancelado ou não cancelável.

Esta abordagem ao cancelamento tem várias vantagens:

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

  • O mesmo pedido de cancelamento pode ser proliferado para qualquer número de ouvintes.

  • O desenvolvedor da API assíncrona tem total controle sobre se o cancelamento pode ser solicitado e quando ele pode entrar em vigor.

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

Acompanhamento dos progressos realizados

Alguns métodos assíncronos expõem o 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é agora. Tal método pode ser consumido em um aplicativo Windows Presentation Foundation (WPF) 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 integrados

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

Task.Run

A Task classe inclui vários Run métodos que permitem descarregar facilmente 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 Run métodos, como a Task.Run(Func<Task>) sobrecarga, existem como abreviação para o TaskFactory.StartNew método. Essa 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);
    });
}

Tais sobrecargas são logicamente equivalentes ao uso do TaskFactory.StartNew método em conjunto com o Unwrap método de extensão na Biblioteca Paralela de Tarefas.

Task.FromResult

Use o método em cenários onde os FromResult dados podem já estar disponíveis e só precisam ser retornados de um método de retorno de tarefa levantado para um 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)
{
    …
}

Tarefa.QuandoTodos

Use o método para aguardar assincronamente em várias operações assíncronas WhenAll que são representadas como tarefas. O método tem várias sobrecargas que suportam um conjunto de tarefas não genéricas ou um conjunto não uniforme de tarefas genéricas (por exemplo, aguardando assincronamente por várias operações de retorno de vazio ou aguardando assíncronamente por vários métodos de retorno de valor onde cada valor pode ter um tipo diferente) e para dar suporte a um conjunto uniforme de tarefas genéricas (como asynchronously waiting for multiple TResult-return methods).

Digamos que você queira enviar mensagens de e-mail para vários clientes. Você pode sobrepor o envio das mensagens para não esperar que uma mensagem seja concluída antes de enviar a próxima. Você também pode descobrir quando as operações de envio foram concluídas e se ocorreram erros:

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

Esse código não manipula explicitamente as exceções que podem ocorrer, mas permite que as exceções se propaguem para fora da await tarefa resultante do WhenAll. Para lidar com as exceções, você pode usar um 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 qualquer operação assíncrona falhar, todas as exceções serão consolidadas em uma AggregateException exceção, que é armazenada no Task que é retornado do WhenAll método. No entanto, apenas uma dessas exceções é propagada pela palavra-chave await . Se você quiser examinar todas as exceções, você pode 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 de download de vários arquivos da Web de forma assíncrona. Nesse caso, todas as operações assíncronas têm tipos de resultados 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 que discutimos no cenário anterior de retorno de vazio:

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

Tarefa.QuandoQualquer

Você pode usar o método para aguardar assincronamente a conclusão de apenas uma das várias operações assíncronas WhenAny representadas como tarefas. Este método serve quatro casos de uso principais:

  • Redundância: Executar uma operação várias vezes e selecionar a que for concluída primeiro (por exemplo, entrar em contato com vários serviços Web de cotação de ações que produzirão um único resultado e selecionar a que for concluída mais rapidamente).

  • Interleaving: Lançar várias operações e esperar que todas sejam concluídas, mas processá-las à medida que são concluídas.

  • Limitação: Permitir que operações adicionais comecem à medida que outras forem concluídas. Trata-se de uma extensão do cenário de intercalação.

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

Redundância

Considere um caso em que você quer tomar uma decisão sobre comprar uma ação. Existem vários serviços web de recomendação de estoque em que você confia, mas dependendo da carga diária, cada serviço pode acabar sendo lento em momentos diferentes. Você pode usar o WhenAny método 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 WhenAlldo , que retorna os resultados desempacotados de todas as tarefas concluídas com êxito, WhenAny retorna a tarefa concluída. Se uma tarefa falhar, é importante saber que falhou e, se uma tarefa for bem-sucedida, é importante saber a qual tarefa o valor de retorno está associado. Portanto, você precisa acessar o resultado da tarefa retornada, ou aguardar mais, como mostra este exemplo.

Tal como acontece com WhenAllo , tem de ser capaz de acomodar exceções. Como você recebe a tarefa concluída de volta, você pode aguardar que a tarefa retornada tenha erros propagados, e try/catch eles apropriadamente, 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 podem 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 WhenAll método, ou você pode decidir que todas as exceções são importantes e devem ser registradas. Para isso, você pode usar continuações para receber uma notificação quando as tarefas forem 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 ainda:

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ê pode querer 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 baixar as 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ê deseja adicioná-los à medida que forem concluídos.

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 a intercalação a um cenário que envolve processamento computacionalmente intensivo nas ThreadPool 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 tantas imagens que os downloads têm que ser limitados; Por exemplo, você deseja que apenas um número específico de downloads aconteça simultaneamente. Para conseguir isso, você pode iniciar um subconjunto das operações assíncronas. À medida que as operações são concluídas, você pode iniciar operações adicionais para substituí-las:

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

Resgate antecipado

Considere que você está aguardando de forma assíncrona a conclusão de uma operação e, ao mesmo tempo, responde à solicitação de cancelamento de um usuário (por exemplo, o usuário clicou em um botão de cancelamento). 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 reativa a interface do usuário assim que você decide resgatar, mas não cancela as operações assíncronas subjacentes. Outra alternativa seria cancelar as operações pendentes quando você decidir resgatar, mas não restabelecer a interface do usuário até que as operações sejam concluídas, potencialmente devido ao término antecipado devido ao pedido 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 resgate antecipado envolve o uso do WhenAny método em conjunto com o Delay método, como discutido na próxima seção.

Task.Delay

Você pode usar o Task.Delay método para introduzir pausas na execução de um método assíncrono. Isso é útil para muitos tipos de funcionalidade, incluindo a criação de loops de sondagem e o atraso no processamento da entrada do usuário por um período de tempo predeterminado. O Task.Delay método também pode ser útil em combinação com Task.WhenAny a implementação de tempos limite em espera.

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

Por exemplo, em seu aplicativo de interface do usuário, digamos que você queira baixar uma imagem e desabilitar a interface do usuário enquanto a imagem está sendo baixada. No entanto, se o download demorar muito, você deseja reativar 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 se aplica 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; }
}

Criando combinadores baseados em tarefas

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

RetryOnFault

Em muitas situações, você pode querer tentar novamente uma operação se uma tentativa anterior falhar. Para código síncrono, você pode criar 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 criar um método auxiliar quase idêntico para operações assíncronas que são 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);
}

Em seguida, você pode usar esse combinador para codificar novas tentativas 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 a RetryOnFault função ainda mais. Por exemplo, a função pode aceitar outra Func<Task> que será invocada entre novas tentativas para determinar quando tentar a operação novamente, 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 usar a função da seguinte forma para aguardar um segundo antes de tentar novamente 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 aproveitar a redundância para melhorar a latência de uma operação e as chances de sucesso. Considere vários serviços da Web que fornecem cotações de ações, mas em vários momentos do dia, cada serviço pode fornecer diferentes níveis de qualidade e tempos de resposta. Para lidar com essas flutuações, você pode emitir solicitações para todos os serviços da Web e, assim que receber uma resposta de um, cancelar as solicitações restantes. Você pode implementar uma função auxiliar para facilitar a implementação desse padrão comum de iniciar várias operações, aguardar qualquer uma e, em seguida, cancelar o restante. A NeedOnlyOne função 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;
}

Em seguida, você pode usar essa função da seguinte maneira:

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 com o uso do WhenAny método para dar suporte a um cenário de intercalação quando você está trabalhando com grandes conjuntos de tarefas. Cada chamada para WhenAny resulta em uma continuação sendo registrada com cada tarefa. Para o número N de tarefas, isso resulta em continuações O(N2) criadas ao longo do tempo de vida da operação de intercalação. Se você estiver trabalhando com um grande conjunto de tarefas, poderá usar um combinador (Interleaved no exemplo a seguir) para resolver 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;
}

Em seguida, você pode usar o combinador para processar os resultados das tarefas à medida que elas são 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, você pode querer aguardar todas as tarefas em um conjunto, a menos que uma delas falhe, caso em que você deseja parar de esperar assim que a exceção ocorrer. Você pode fazer isso com um método combinador, 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;
}

Criando estruturas de dados baseadas em tarefas

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

AsyncCache

Um aspeto importante de uma tarefa é que ela pode ser entregue a vários consumidores, todos os quais podem aguardá-la, registrar continuações com ela, obter seu resultado ou exceções (no caso de Task<TResult>), e assim por diante. Isso faz com Task que seja 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 sobre 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 usa um TKey e retorna um Task<TResult>. Todos os valores acessados anteriormente do cache são armazenados no dicionário interno e o AsyncCache garante que apenas uma tarefa seja gerada por chave, mesmo que o cache seja acessado simultaneamente.

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

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

Em seguida, você pode usar esse cache em métodos assíncronos sempre que precisar do conteúdo de uma página da Web. A AsyncCache classe garante que você esteja baixando o menor número possível de páginas e armazena em cache os 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

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

Aqui está uma estrutura de dados simples, construída sobre tarefas, que permite que 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 System.Threading.Tasks.Dataflow namespace inclui o BufferBlock<T> tipo, que você pode usar de maneira semelhante, mas sem precisar 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);
}

Nota

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

Consulte também