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

Usando Task.WhenAny, você pode iniciar várias tarefas ao mesmo tempo e processá-las individualmente conforme elas forem concluídas, em vez de processá-las na ordem em que foram 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 esperada para WhenAny retorna a tarefa na coleção de tarefas que concluir o 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 Visual Studio Code.

Criar aplicativo de exemplo

Criar um novo aplicativo de console do .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ódigo e substitua o código existente por este:

using System.Diagnostics;

namespace ProcessTasksAsTheyFinish;

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

Adicionar campos

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

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

static readonly IEnumerable<string> s_urlList = new string[]
{
    "https://video2.skills-academy.com",
    "https://video2.skills-academy.com/aspnet/core",
    "https://video2.skills-academy.com/azure",
    "https://video2.skills-academy.com/azure/devops",
    "https://video2.skills-academy.com/dotnet",
    "https://video2.skills-academy.com/dynamics365",
    "https://video2.skills-academy.com/education",
    "https://video2.skills-academy.com/enterprise-mobility-security",
    "https://video2.skills-academy.com/gaming",
    "https://video2.skills-academy.com/graph",
    "https://video2.skills-academy.com/microsoft-365",
    "https://video2.skills-academy.com/office",
    "https://video2.skills-academy.com/powershell",
    "https://video2.skills-academy.com/sql",
    "https://video2.skills-academy.com/surface",
    "https://video2.skills-academy.com/system-center",
    "https://video2.skills-academy.com/visualstudio",
    "https://video2.skills-academy.com/windows",
    "https://video2.skills-academy.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.

Atualize o ponto de entrada do aplicativo

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

static Task Main() => SumPageSizesAsync();

O método atualizado Main agora é considerado um Async main, 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 método Main, adicione o método SumPageSizesAsync:

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 loop while remove uma das tarefas em cada iteração. Depois que todas as tarefas forem concluídas, 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 no código a seguir retorna um Task<TResult>, em que 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 loop while executa as seguintes etapas para cada tarefa na coleção:

  1. Espera uma chamada para WhenAny, com o objetivo de identificar a primeira tarefa na coleção que concluiu o download.

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

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

    total += await finishedTask;
    

Adicionar método de processo

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

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 instância client 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 tamanhos baixados não aparecem sempre na mesma ordem.

Cuidado

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

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

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

Confira também