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

Ao utilizar Task.WhenAnyo , pode iniciar múltiplas tarefas ao mesmo tempo e processá-las uma a uma à medida que são concluídas, em vez de processá-las pela ordem em que são iniciadas.

O exemplo seguinte utiliza uma consulta para criar uma coleção de tarefas. Cada tarefa transfere o conteúdo de um site especificado. Em cada iteração de um ciclo de tempo, uma chamada aguardada para WhenAny devolver a tarefa na coleção de tarefas que termina a transferência primeiro. Essa tarefa é removida da coleção e processada. O ciclo repete-se até que a coleção não contenha mais tarefas.

Pré-requisitos

Pode seguir este tutorial com uma das seguintes opções:

  • Visual Studio 2022 com a carga de trabalho de desenvolvimento de ambiente de trabalho .NET instalada. O SDK .NET é instalado automaticamente quando seleciona esta carga de trabalho.
  • O SDK .NET com um editor de código à sua escolha, como o Visual Studio Code.

Criar aplicação de exemplo

Crie uma nova aplicação de consola .NET Core. Pode criar uma com o comando da nova consola do dotnet ou a partir do Visual Studio.

Abra o ficheiro 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 seguintes:

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 pedidos HTTP e receber respostas HTTP. O s_urlList contém todos os URLs que a aplicação planeia processar.

Atualizar ponto de entrada da aplicação

O ponto de entrada principal na aplicação de consola é 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, o que permite um ponto de entrada assíncrono no executável. É 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 ciclo remove uma das tarefas em cada iteração. Após a conclusão de cada tarefa, o ciclo termina. O método começa por instanciar e iniciar um Stopwatch. Em seguida, inclui uma consulta que, quando executada, cria uma coleção de tarefas. Cada chamada para ProcessUrlAsync no seguinte código devolve um Task<TResult>, em que TResult é um número inteiro:

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

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

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

O while ciclo executa os seguintes passos para cada tarefa na coleção:

  1. Aguarda uma chamada para para WhenAny identificar a primeira tarefa na coleção que terminou a transferência.

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

    downloadTasks.Remove(finishedTask);
    
  3. finishedTaskAguarda , que é devolvido por uma chamada para ProcessUrlAsync. A finishedTask variável é onde TResultTask<TResult> é um número inteiro. A tarefa já está concluída, mas aguarda-a para obter a duração do site transferido, como mostra o exemplo seguinte. Se a tarefa tiver falhas, await apresentará a primeira exceção subordinada armazenada no AggregateException, ao contrário de ler a Task<TResult>.Result propriedade , o que geraria 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 especificado, o método utilizará a client instância fornecida para obter a resposta como .byte[] O comprimento é devolvido depois de o URL e o comprimento terem sido escritos na consola.

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

Atenção

Pode utilizar WhenAny num ciclo, conforme descrito no exemplo, para resolver problemas que envolvem um pequeno número de tarefas. No entanto, outras abordagens são mais eficientes se tiver um grande número de tarefas para processar. Para obter mais informações e exemplos, veja Processar tarefas à medida que são concluídas.

Exemplo completo

O código seguinte é o texto completo do ficheiro Program.cs do 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

Ver também