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:
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);
Remove a tarefa da coleção.
downloadTasks.Remove(finishedTask);
Espera
finishedTask
, que é retornado por uma chamada paraProcessUrlAsync
. A variávelfinishedTask
é uma Task<TResult> em queTResult
é 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 noAggregateException
, ao contrário da leitura da propriedade Task<TResult>.Result, que lançaria oAggregateException
.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