Compartilhar via


Processar tarefas assíncronas conforme elas são concluídas (C#)

Ao usar Task.WhenAny, você pode iniciar várias tarefas ao mesmo tempo e processá-las uma a uma, pois elas são concluídas em vez de processá-las na ordem em que elas são iniciadas.

O exemplo a seguir usa uma consulta para criar uma coleção de tarefas. Cada tarefa baixa o conteúdo de um site especificado. Em cada iteração de um loop de tempo, uma chamada aguardada para WhenAny retornar a tarefa na coleção de tarefas que conclui seu download primeiro. Essa tarefa é removida da coleção e processada. O loop é repetido até que a coleção não contenha mais tarefas.

Pré-requisitos

Você pode seguir este tutorial usando uma das seguintes opções:

  • Visual Studio 2022 com a carga de trabalho Desenvolvimento de área de trabalho do .NET instalada. O SDK do .NET é instalado automaticamente quando você seleciona essa carga de trabalho.
  • O SDK do .NET com um editor de código de sua escolha, como o Visual Studio Code.

Criar aplicativo de exemplo

Crie um novo aplicativo de console do .NET Core. Você pode criar um usando o novo comando de console do dotnet ou do Visual Studio.

Abra o arquivo Program.cs no editor de código e substitua o código existente por este código:

using System.Diagnostics;

namespace ProcessTasksAsTheyFinish;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Adicionar campos

Na definição de Program classe, adicione os dois campos a seguir:

static readonly HttpClient s_client = new HttpClient
{
    MaxResponseContentBufferSize = 1_000_000
};

static readonly IEnumerable<string> s_urlList = new string[]
{
    "https://learn.microsoft.com",
    "https://learn.microsoft.com/aspnet/core",
    "https://learn.microsoft.com/azure",
    "https://learn.microsoft.com/azure/devops",
    "https://learn.microsoft.com/dotnet",
    "https://learn.microsoft.com/dynamics365",
    "https://learn.microsoft.com/education",
    "https://learn.microsoft.com/enterprise-mobility-security",
    "https://learn.microsoft.com/gaming",
    "https://learn.microsoft.com/graph",
    "https://learn.microsoft.com/microsoft-365",
    "https://learn.microsoft.com/office",
    "https://learn.microsoft.com/powershell",
    "https://learn.microsoft.com/sql",
    "https://learn.microsoft.com/surface",
    "https://learn.microsoft.com/system-center",
    "https://learn.microsoft.com/visualstudio",
    "https://learn.microsoft.com/windows",
    "https://learn.microsoft.com/maui"
};

Expõe HttpClient a capacidade de enviar solicitações HTTP e receber respostas HTTP. Contém s_urlList todas as URLs que o aplicativo planeja processar.

Atualizar o ponto de entrada do aplicativo

O principal ponto de entrada no aplicativo de console é o Main método. Substitua o método existente pelo seguinte:

static Task Main() => SumPageSizesAsync();

O método atualizado Main agora é considerado um principal assíncrono, que permite um ponto de entrada assíncrono no executável. Ele é expresso como uma chamada para SumPageSizesAsync.

Criar o método de tamanhos de página de soma assíncrona

Abaixo do Main método, adicione o SumPageSizesAsync método:

static async Task SumPageSizesAsync()
{
    var stopwatch = Stopwatch.StartNew();

    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

    int total = 0;
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:  {total:#,#}");
    Console.WriteLine($"Elapsed time:          {stopwatch.Elapsed}\n");
}

O while loop remove uma das tarefas em cada iteração. Depois que cada tarefa for concluída, o loop terminará. O método começa instanciando e iniciando um Stopwatch. Em seguida, ele inclui uma consulta que, quando executada, cria uma coleção de tarefas. Cada chamada para ProcessUrlAsync o código a seguir retorna um Task<TResult>, em que TResult está um inteiro:

IEnumerable<Task<int>> downloadTasksQuery =
    from url in s_urlList
    select ProcessUrlAsync(url, s_client);

Devido à execução adiada com o LINQ, você chama Enumerable.ToList para iniciar cada tarefa.

List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

O while loop executa as seguintes etapas para cada tarefa na coleção:

  1. Aguarda uma chamada para WhenAny identificar a primeira tarefa na coleção que concluiu seu download.

    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    
  2. Remove essa tarefa da coleção.

    downloadTasks.Remove(finishedTask);
    
  3. finishedTaskAguarda, que é retornado por uma chamada para ProcessUrlAsync. A finishedTask variável é um Task<TResult> local TResult em que é um inteiro. A tarefa já está concluída, mas você a aguarda para recuperar o comprimento do site baixado, como mostra o exemplo a seguir. Se a tarefa tiver falha, await gerará a primeira exceção filho armazenada no , ao contrário da AggregateExceptionleitura da Task<TResult>.Result propriedade, que lançaria o AggregateException.

    total += await finishedTask;
    

Adicionar método de processo

Adicione o seguinte ProcessUrlAsync método abaixo do SumPageSizesAsync método:

static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
    byte[] content = await client.GetByteArrayAsync(url);
    Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

    return content.Length;
}

Para qualquer URL fornecida, o método usará a client instância fornecida para obter a resposta como um byte[]. O comprimento é retornado depois que a URL e o comprimento são gravados no console.

Execute o programa várias vezes para verificar se os comprimentos baixados nem sempre aparecem na mesma ordem.

Cuidado

Você pode usar WhenAny em um loop, conforme descrito no exemplo, para resolver problemas que envolvem um pequeno número de tarefas. No entanto, outras abordagens serão mais eficientes se você tiver um grande número de tarefas a serem processadas. Para obter mais informações e exemplos, consulte As tarefas de processamento à medida que elas são concluídas.

Simplificar a abordagem usando Task.WhenEach

O while loop implementado no SumPageSizesAsync método pode ser simplificado usando o novo Task.WhenEach método introduzido no .NET 9 chamando-o em await foreach loop.
Substitua o loop implementado while anteriormente:

    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

com o simplificado await foreach:

    await foreach (Task<int> t in Task.WhenEach(downloadTasks))
    {
        total += await t;
    }

Essa nova abordagem permite não chamar Task.WhenAny mais repetidamente para chamar manualmente uma tarefa e remover a que termina, pois Task.WhenEach itera por meio da tarefa em uma ordem de conclusão.

Exemplo completo

O código a seguir é o texto completo do arquivo Program.cs para o exemplo.

using System.Diagnostics;

HttpClient s_client = new()
{
    MaxResponseContentBufferSize = 1_000_000
};

IEnumerable<string> s_urlList = new string[]
{
    "https://learn.microsoft.com",
    "https://learn.microsoft.com/aspnet/core",
    "https://learn.microsoft.com/azure",
    "https://learn.microsoft.com/azure/devops",
    "https://learn.microsoft.com/dotnet",
    "https://learn.microsoft.com/dynamics365",
    "https://learn.microsoft.com/education",
    "https://learn.microsoft.com/enterprise-mobility-security",
    "https://learn.microsoft.com/gaming",
    "https://learn.microsoft.com/graph",
    "https://learn.microsoft.com/microsoft-365",
    "https://learn.microsoft.com/office",
    "https://learn.microsoft.com/powershell",
    "https://learn.microsoft.com/sql",
    "https://learn.microsoft.com/surface",
    "https://learn.microsoft.com/system-center",
    "https://learn.microsoft.com/visualstudio",
    "https://learn.microsoft.com/windows",
    "https://learn.microsoft.com/maui"
};

await SumPageSizesAsync();

async Task SumPageSizesAsync()
{
    var stopwatch = Stopwatch.StartNew();

    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

    int total = 0;
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:    {total:#,#}");
    Console.WriteLine($"Elapsed time:              {stopwatch.Elapsed}\n");
}

static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
    byte[] content = await client.GetByteArrayAsync(url);
    Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

    return content.Length;
}

// Example output:
// https://learn.microsoft.com                                      132,517
// https://learn.microsoft.com/powershell                            57,375
// https://learn.microsoft.com/gaming                                33,549
// https://learn.microsoft.com/aspnet/core                           88,714
// https://learn.microsoft.com/surface                               39,840
// https://learn.microsoft.com/enterprise-mobility-security          30,903
// https://learn.microsoft.com/microsoft-365                         67,867
// https://learn.microsoft.com/windows                               26,816
// https://learn.microsoft.com/maui                               57,958
// https://learn.microsoft.com/dotnet                                78,706
// https://learn.microsoft.com/graph                                 48,277
// https://learn.microsoft.com/dynamics365                           49,042
// https://learn.microsoft.com/office                                67,867
// https://learn.microsoft.com/system-center                         42,887
// https://learn.microsoft.com/education                             38,636
// https://learn.microsoft.com/azure                                421,663
// https://learn.microsoft.com/visualstudio                          30,925
// https://learn.microsoft.com/sql                                   54,608
// https://learn.microsoft.com/azure/devops                          86,034

// Total bytes returned:    1,454,184
// Elapsed time:            00:00:01.1290403

Consulte também