Partilhar via


Processar tarefas assíncronas à medida que são concluídas (C#)

Task.WhenAnyUsando o , você pode iniciar várias tarefas ao mesmo tempo e processá-las uma a uma à medida que são concluídas, em vez de processá-las na ordem em que 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 while, uma chamada aguardada para WhenAny retornar a tarefa na coleção de tarefas que termina seu download primeiro. Essa tarefa é removida da coleção e processada. O loop se repete 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 de desenvolvimento desktop .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 .NET Core. Você pode criar um usando o comando dotnet new console ou do Visual Studio.

Abra o arquivo Program.cs no editor de códigos 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"
};

O HttpClient expõe a capacidade de enviar solicitações HTTP e receber respostas HTTP. O s_urlList contém 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. É expresso como um apelo à 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. Após a conclusão de cada tarefa, o loop termina. O método começa instanciando e iniciando um Stopwatcharquivo . Em seguida, inclui uma consulta que, quando executada, cria uma coleção de tarefas. Cada chamada para ProcessUrlAsync no código a seguir retorna um Task<TResult>, onde TResult é 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 terminou seu download.

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

    downloadTasks.Remove(finishedTask);
    
  3. Aguarda finishedTask, que é retornado por uma chamada para ProcessUrlAsync. A finishedTask variável é um Task<TResult> onde TResult é um inteiro. A tarefa já está concluída, mas você aguarda para recuperar o comprimento do site baixado, como mostra o exemplo a seguir. Se a tarefa estiver com defeito, await lançará 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 determinado, o método usará a client instância fornecida para obter a resposta como um byte[]arquivo . O comprimento é retornado depois que o 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.

Atenção

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

Simplifique 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 mais chamar Task.WhenAny repetidamente para chamar manualmente uma tarefa e remover a que termina, porque Task.WhenEach itera através da tarefa em uma ordem de sua 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