C# -  Programação assíncrona e paralela com Task


 Hoje vamos falar sobre a classe Task que representa uma operação assíncrona e o seu uso nas   operações simultâneas em C#.

A programação concorrente ou simultânea é usada para dois tipos de tarefas: tarefas I/O-bound ou operações de input/output e CPU-bound que indica operações que são altamente dependes da CPU.

Assim solicitar dados de uma rede, acessar um banco de dados ou ler e gravar são tarefas vinculadas a IO e  tarefas de CPU são tarefas que são computacionalmente caras, como cálculos matemáticos ou processamento gráfico.

As operações assíncronas são adequadas para tarefas vinculadas a I/O e a operações paralelas são adequadas para tarefas vinculadas à CPU. Ao contrário de outras linguagens, a classe Task pode ser usada para operações assíncronas e paralelas.

Aqui temos duas opções :

Task
Taskt<TResult>


Task representa uma operação simultânea, enquanto Task<TResult> representa uma operação simultânea que pode retornar um valor e ambas são executadas de forma assíncrona.

O método Task.Run é usado para executar o código vinculado à CPU simultaneamente; idealmente em paralelo. Ele enfileira o trabalho especificado para execução no ThreadPool e retorna um handle Task ou  Task<TResult> para esse trabalho.

A plataforma .NET contém vários métodos, como StreamReader.ReadLineAsync ou HttpClient.GetAsync, que executam código vinculado a E/S de forma assíncrona. Eles são usados em conjunto com as palavras-chave async/await.

Nota: É recomendado usar Task.Run ao invés de new Task(); task.Start().

Usando Task.Run

Este método coloca a tarefa a ser realizada na fila para execução no ThreadPool e retorna uma Task ou Task<T>.

Assim, o método Task.Run coloca uma tarefa em um thread diferente sendo adequado para tarefas vinculadas à CPU.

Como exemplo de uso de Task.Run temos o trecho de código abaixo definido em um programa Console no .NET 7.0 usando o recurso Top Level Statement:

Console.WriteLine($"Main thread {getThreadId()} início");

Task.Run(() =>
{
   Console.WriteLine(
"Aguardando...");
   Console.WriteLine(
$"Thread {getThreadId()} início");
   Thread.Sleep(3000);
   Console.WriteLine(
$"Thread {getThreadId()} fim");
});

Console.WriteLine($"Main thread {getThreadId()} fim");
Console.ReadLine();


int
getThreadId()
{

   return
Thread.CurrentThread.ManagedThreadId;
}

Resultado:

Observe que a thread principal termina antes da tarefa gerada. Para ver a tarefa concluída, usamos o Console.ReadLine que aguarda a entrada do usuário.

A seguir temos um exemplo de Task<T> que representa uma tarefa que retorna um resultado:

Console.WriteLine("### Usando Task<T> ###\n");

Task<
int> tarefa = Task.Run(() =>
{
   Thread.Sleep(3000);
  
return 2 + 3;
});

var resultado = await tarefa;

Console.WriteLine($"2 + 3 = {resultado}");

Console.ReadKey();

Resultado :

Este código mostra como esperar por uma tarefa que retorna um resultado do processamento.

Aqui usamos o operador await que suspende a avaliação do método assíncrono enquanto a operação assíncrona representada por seu operando não for concluída.

Quando a operação assíncrona for concluída, o operador await retornará o resultado da operação, se houver.

Usando Task.Delay

O método Task.Delay cria uma tarefa que é concluída após um atraso ou delay.

Console.WriteLine("etapa 1");
await ExecutaTarefa();
Console.WriteLine("etapa 2");
async Task ExecutaTarefa()
{
    await Task.Delay(3000);
    Console.WriteLine("tarefa concluída após 3s");
}
Console.ReadKey();

Resultado:

O método ExecutaTarefa que processa a tarefa precisa utilizar a palavra-chave async e a chamada do método precisa usar o operador await.

O método Task.Delay cria uma nova tarefa, que dorme por três segundos.

O operador await aguarda a conclusão da tarefa e bloqueia a execução do programa principal até que a tarefa seja concluída. (Ele suspende a avaliação do método async até que a operação assíncrona seja concluída)

As palavras-chave async e await são a parte central da programação assíncrona. Ao usar essas duas palavras-chave, você poderá usar recursos da plataforma .NET ou do Windows Runtime para criar um método assíncrono de forma bem simples.

Usando o método Task.WaitAll

O método Task.WaitAll espera que todos os objetos Task fornecidos concluam a execução.

using System.Diagnostics;

Console.WriteLine("## Usando Task.WaitAll ##\n");

var sw = new Stopwatch();
sw.Start();

Task.WaitAll(T1Async(), T2Async(), T3Async());

sw.Stop();

var tempo = sw.ElapsedMilliseconds;
Console.WriteLine(
$"\nTempo gasto : {tempo} ms");

async Task T1Async()
{
 
await Task.Delay(4000);
  Console.WriteLine(
"T1Async concluído");
}

async
Task T2Async()
{
 
await Task.Delay(7000);
  Console.WriteLine(
"T2Async concluído");
}

async
Task T3Async()
{
 
await Task.Delay(2000);
  Console.WriteLine(
"T3Async concluído");
}

Console.ReadKey();

Resultado:

Neste código estamos exeutando 3 métodos assíncronos onde usamos as palavras-chave async/await.

Dessa forma caracterizar os métodos assíncronos da seguinte forma:

  1. A assinatura do método deve incluir o modificador async;

  2. O método deve ter um tipo de retorno da Task<TResult>, Task ou void;

  3. As declarações de método devem incluir pelo menos uma única expressão await - isso diz ao compilador que o método precisa ser suspenso enquanto a operação aguardada estiver ocupada.

  4. Por último, o nome do método deve terminar com o sufixo "async" (mesmo que isso seja mais convencional do que o necessário).

Desta forma para criar código assíncrono usamos as palavras chaves async e await onde por padrão um método modificado por uma palavra-chave async contém pelo menos uma expressão await.

Usando o método Task.ContinueWith

O método Task.ContinueWith cria uma continuação que é executada de forma assíncrona quando o Task<TResult> de destino for concluído. Este método possui diversas sobrecargas.

Console.WriteLine("## Usando Task.ContinueWith ##\n");

Task<int> tarefa =
    Task.Run(() =>
    ExecutaTarefa())
    .ContinueWith<
int>((x) => x.Result * 2);

var resultado = await tarefa;

Console.WriteLine(resultado);

int ExecutaTarefa()
{
  
int x = 1;
  
int y = 2;
  
int z = 3;
   Thread.Sleep(1000);
  
return x + y + z;
}

Console.ReadKey();

Resultado:

Neste código estamos encadeando duas operações usando o método ContinueWith de forma que o método vai somar os números 1,2 e 3 e a seguir vai multiplar por 2.

Executando múltiplos request assíncronos

A classe HttpClient é usada para enviar solicitações HTTP e receber respostas HTTP do recurso especificado. Abaixo temos um exemplo de uso desta classe:

using System.Diagnostics;

Console.WriteLine("## Realizando vários requests com HttpClient ##\n");

var urls = new string[] { "https://macoratti.net", "http://twitter.com",
"https://uol.com", "http://microsoft.com", "https://github.com" };

var sw = new Stopwatch();
sw.Start();

using var client = new HttpClient();

var tarefas = new List<Task<HttpResponseMessage>>();

foreach (var url in urls)
{
   Console.WriteLine(
$"GetAsync() na url {url}");
   tarefas.Add(client.GetAsync(url));
}

Console.WriteLine("\nAguardando o resultado...\n");
Task.WaitAll(tarefas.ToArray());

var dados = new List<HttpResponseMessage>();

foreach (var tarefa in tarefas)
{
  dados.Add(
await tarefa);
}

sw.Stop();
var
tempo = sw.ElapsedMilliseconds;

foreach (var resultado in dados)
{
  Console.WriteLine(resultado.StatusCode);
}

Console.WriteLine($"\nTempo gasto: {tempo} ms");

Console.ReadKey();

Resultado:

Entendendo o código:

Enviamos solicitações GET assíncronas para várias páginas da Web e obtemos seus códigos de status de resposta.

tarefas.Add(client.GetAsync(url));

O GetAsync envia um request GET para a URL especificada e retorna o corpo da resposta em uma operação assíncrona. Ele retorna uma nova tarefa e a tarefa é adicionada à lista de tarefas.

Task.WaitAll(tarefas.ToArray());

O método Task.WaitAll espera que todas as tarefas fornecidas concluam a execução.

dados.Add(await tarefa);

O operador await aguarda o resultado da operação que é exibido no laço foreach:

foreach (var resultado em dados)
{
    Console.WriteLine(resultado.StatusCode);
}

Estamos exibindo o código de status da operação , onde Ok significa que a operação foi concluida com sucesso.

Para concluir, não pense que você pode sair por ai usando async e await indiscriminadamente. Existem algumas regras básicas que devem ser seguidas :

Obs:  Esteja atento e verifique se houve alguma mudança nos comportamentos acima em novas versões do C#.

E estamos conversados...

Disse Jesus: "Olhai para as aves do céu, que nem semeiam, nem segam, nem ajuntam em celeiros; e vosso Pai celestial as alimenta. Não tendes vós muito mais valor do que elas?"
Matheus 6:26

Referências:


José Carlos Macoratti