C# - Programação Assíncrona - II
Vamos recordar conceitos importantes da programação assíncrona na linguagem C# |
Continuando o artigo anterior vamos agora por em prática a teoria da programação assíncrona.
Programação Síncrona
Antes de entramos na programação assíncrona, vamos entender o que é programação síncrona usando o seguinte exemplo de código em uma aplicação Console - AppSincrona - no .NET 7.0 usando as instruções de alto nível :
Console.WriteLine("\n### Programação Síncrona ###");
Console.WriteLine("\nExecutando processo de longa duração...");
ProcessoLongaDuracao();
Console.WriteLine("\nExecutando processo de curta duração...");
ProcessoCurtaDuracao();
Console.WriteLine("\ntarefa encerrada.");
Console.ReadKey();
static void ProcessoLongaDuracao()
{
Console.WriteLine("-Processo de longa duração iniciado...");
System.Threading.Thread.Sleep(5000);
Console.WriteLine("-Processo de longa duração concluído.");
}
static void ProcessoCurtaDuracao()
{
Console.WriteLine("-Processo de curta duração iniciado...");
System.Threading.Thread.Sleep(800);
Console.WriteLine("-Processo de curta duração concluído.");
}
|
A execução deste código vai gerar o seguinte resultado:
No exemplo acima, o método
ProcessoLongaDuracao()
é uma tarefa de execução longa, como ler um arquivo do servidor, chamar uma API
da web que retorna uma grande quantidade de dados ou carregar ou baixar um
arquivo grande.
Ela vai demorar um pouco mais para executar e para simular isso estamos retarando a execução em 5 segundos usando o código Thread.Sleep(5000) apenas para mostrar o longo tempo de execução.
O método
ProcessoCurtaDuracao()
é um método simples que é executado após o método
ProcessoLongaDuracao()
e que tem um retardo ou delay de apenas 0.8 segundos.
O programa acima é executado de forma síncrona. Isso significa que a execução
começa a partir do método método
ProcessoLongaDuracao()
e depois o método
ProcessoCurtaDuracao().
Durante a execução, um aplicativo é bloqueado e deixa de responder (você pode
ver isso principalmente em aplicativos baseados no Windows). Isso é chamado
de programação síncrona, onde a execução não vai para a próxima linha até que a
linha atual seja executada completamente.
Programação Assíncrona
Agora vamos criar um novo projeto Console chamado
AppAssincrona no VS 2022 usando o .NET 7.0.
Após criar o projeto vamos converter o modo Top Level Statament para o estilo Program.Main clicando com o botão direito do mouse sobre o código e selecionando esta opção :
Ao final teremos a seguinte exibição na IDE:
Vamos agora alterar este código incluindo o código abaixo na classe Program:
using System.Diagnostics;
internal class Program
{
static readonly Stopwatch timer = new Stopwatch();
public static void Main(string[] args)
{
timer.Start();
Console.WriteLine("\nProgram Inicio - " + timer.Elapsed.ToString() + "\n");
Task1();
Task2();
Console.WriteLine("\nProgram Fim - " + timer.Elapsed.ToString());
timer.Stop();
Console.ReadKey();
}
private static async Task Task1Async()
{
Console.WriteLine("Async Task 1 Iniciada - " + timer.Elapsed.TotalSeconds.ToString());
await Task.Delay(2000);
Console.WriteLine("Async Task 1 Completada - " + timer.Elapsed.TotalSeconds.ToString());
}
private static async Task Task2Async()
{
Console.WriteLine("Async Task 2 Iniciada - " + timer.Elapsed.TotalSeconds.ToString());
await Task.Delay(3000);
Console.WriteLine("Async Task 2 Completada - " + timer.Elapsed.TotalSeconds.ToString());
}
private static void Task1()
{
Console.WriteLine("Task 1 Iniciada - " + timer.Elapsed.TotalSeconds.ToString());
Thread.Sleep(2000);
Console.WriteLine("Task 1 Completada - " + timer.Elapsed.TotalSeconds.ToString());
}
private static void Task2()
{
Console.WriteLine("Task 2 Iniciada - " + timer.Elapsed.TotalSeconds.ToString());
Thread.Sleep(3000);
Console.WriteLine("Task 2 Completada - " + timer.Elapsed.TotalSeconds.ToString());
}
}
|
No código acima,
adicionamos os métodos Task1 e Task2 e, para
ambos os métodos, adicionamos versões sincronizadas e assíncronas dos métodos.
Esses métodos de tarefa são métodos de tarefa fictícios que simulam uma tarefa
adicionando atraso à execução. Em aplicações reais, estes serão códigos para
operações de arquivo ou operação de banco de dados ou tarefas de computação.
Nestes métodos de tarefa (Task1 & Task2)
também estamos imprimindo a hora de início do método e também a hora de
conclusão do método para que possamos ter uma boa ideia de como está acontecendo
a execução desses métodos.
No método principal, chamaremos esses métodos de tarefa (Task1
e Task2) um após o outro. Primeiro, chamaremos a versão de
sincronização dos métodos de tarefa e verificaremos a saída
Também no método main, adicionamos código para
imprimir a hora de início do programa e a hora de término para que saibamos
quando esses métodos foram chamados após o início do aplicativo.
Para registrar o tempo no método Main do programa e também nos métodos Task,
usamos a classe stopwatch para iniciar o cronômetro
e, em seguida, imprimimos o tempo decorrido do cronômetro em cada evento.
Fizemos uso das seguintes palavras-chave para tornar os métodos assíncronos :
async – Quando adicionamos esta palavra-chave na
definição do método(antes do nome do método), ela nos permite chamar este método
de forma assíncrona, ou seja, a palavra-chave async
é usada para qualificar uma função como uma função assíncrona
await – Quando queremos chamar um método assíncrona
de forma assíncrona, usamos esta palavra-chave await.
Essa palavra-chave é adicionada abaixo da chamada do método.
Vamos primeiro verificar a saída do código acima com a versão de sincronização
dos métodos Task. Depois de executar o código
acima, podemos ver a saída abaixo na janela do console.
Os resultados acima são os esperados, ou seja, Task1 começou assim que o programa foi iniciado e levou cerca de 2 segundos para ser concluído, pois tem um atraso de 2 segundos. Imediatamente após a conclusão da Task1, a Task2 começou a ser executada e levou cerca de 3 segundos para ser concluída, pois tem um atraso de 3 segundos. Portanto, a execução total do programa levou aproximadamente 5 segundos para ser concluída.
Modificar o método Main para aguardar em vez
de bloquear
Vimos a saída do código acima com a versão de sincronização dos métodos Task,
que é uma prática ruim de bloqueio, pois esse código impede que a thread que o
executa faça qualquer outro trabalho.
Agora vamos modificar o método Main no
Program.cs para chamar a versão assíncrona dos
métodos Task (Task1Async & Task2Async) para
que as threads não sejam bloqueados enquanto as tarefas estão em execução.
Faremos uso da palavra-chave await com uma
versão assíncrona dos métodos de tarefa que fornece uma maneira sem bloqueio de
iniciar uma tarefa.
Modifique o código no método Main no arquivo
Program.cs conforme mostrado abaixo.
public async static Task Main(string[] args)
{
timer.Start();
Console.WriteLine("Program Início - " + timer.Elapsed.ToString());
await Task1Async();
await Task2Async();
Console.WriteLine("Program Fim - " + timer.Elapsed.ToString());
timer.Stop();
}
|
Modificamos o método Main para usar uma versão assíncrona dos métodos de tarefa (Task1Async e Task2Async). Para chamar esses métodos de forma assíncrona, estamos usando palavras-chave await antes de chamar os métodos conforme mostrado no código acima.
Nota: A partir do C# 7.1, você pode usar
o modificador async no método Main, desde que o método retorne uma tarefa (Task)
ou uma tarefa genérica (Task<T>).
Depois de executar o código acima, podemos ver a saída abaixo no console:
Os resultados
acima são semelhantes à versão de sincronização, ou seja, a execução total do
programa levou aproximadamente 5 segundos para ser concluída. ou seja, ambos os
métodos Task1Async e Task2Async foram executados
sequencialmente. Isso ocorre porque o código ainda precisa aproveitar a
programação assíncrona no .NET.
O código acima não bloqueia durante a execução da Async
Task1, mas não iniciará a Async Task2 até
que a Async Task1 seja concluída. Se você não conseguir chamar
Async Task1 e Async Task2 em paralelo devido à
dependência, essa é a única alteração necessária para executar o aplicativo no
modo sem bloqueio. Um aplicativo com uma interface gráfica ou GUI não entrará em
um estado de não resposta apenas por essa alteração.
No entanto, neste programa, podemos executar Async Task1 e
Async Task2 em paralelo, pois não há dependência entre eles, ou seja,
você não deseja que cada método de Async Task seja executado
sequencialmente. Então, vamos fazer a alteração do código para iniciar a
Asycn Task 2 antes de aguardar a conclusão de
Async Task 1.
Iniciando Async Task1 e Async Task2
simultaneamente
Em aplicativos reais, se você tiver tarefas que podem ser executadas
independentemente, você deseja iniciar essas tarefas imediatamente para que
possam ser executadas em paralelo. A execução de operações em paralelo fará tudo
em menos tempo em comparação com a execução sequencial.
Para poder executar as operações em segundo plano temos que usar a classe Task.
A classe Task é uma das principais classes utilizadas na programação assíncrona do C#. Ela permite que uma tarefa seja executada em segundo plano, sem bloquear a thread principal do programa. Isso é especialmente útil em aplicações que precisam realizar operações longas ou bloqueantes, como leitura de arquivos, acesso a banco de dados, ou chamadas de rede.
Ao utilizar a classe Task, o programador pode criar uma nova tarefa, que será executada em segundo plano
Assim vamos precisar usar a classe Task e manter o objeto Task (System.Threading.Tasks.Task) que representa a operação.
Você terá que
aguardar usando a palavra-chave await cada
objeto de Task (tarefa) antes de trabalhar para a
conclusão do programa.
Vamos fazer essas alterações no método Main em Program.cs
conforme mostrado abaixo
public static async Task Main(string[] args)
{
timer.Start();
Console.WriteLine("Program Inicio - " + timer.Elapsed.ToString());
Task task1Task = Task1Async();
Task task2Task = Task2Async();
await task1Task;
await task2Task;
Console.WriteLine("Program Fim - " + timer.Elapsed.ToString());
timer.Stop();
Console.ReadKey();
}
|
Esse código em C# cria duas tarefas assíncronas que serão executadas em segundo plano: task1Task e task2Task, através das chamadas dos métodos Task1Async() e Task2Async(), respectivamente.
Armazenamos as tarefas (task1Task e task2Task) para as operações (Task1Async e Task2Async) quando elas iniciam.
Em seguida, o código aguarda a conclusão da primeira tarefa, task1Task, usando a palavra-chave await, o que significa que o restante do código aguardará até que essa tarefa termine de executar.
Depois que a primeira tarefa é concluída, o código aguarda a conclusão da segunda tarefa, task2Task, também usando a palavra-chave await.
Depois de executar o código acima, podemos ver a saída abaixo no console
Agora, vemos na saída acima que temos o benefício de desempenho da programação assíncrona, ou seja, nosso programa total foi concluído em aproximadamente 3 segundos, em vez dos 5 segundos que foram obtidos anteriormente.
Podemos ver que async task1 e async task2 iniciaram quase ao mesmo tempo quando o programa foi iniciado. Tanto a Task1Async quanto a Task2Async foram executadas em paralelo, portanto houve uma melhora no tempo de execução do código.
Aguardando a tarefa com eficiência
Vamos agora modificar o método Main para aguardar a tarefa com eficiência fazendo a seguinte alteração:
No código acima, estamos aguardando várias tarefas, portanto, no código, há mais de 1 instrução usando a palavra-chave await para aguardar a execução do método.
Agora, se tivermos muitas tarefas, haverá uma série de instruções de com await. Este código pode ser melhorado fazendo uso de métodos disponíveis na classe Task.
public static async Task Main(string[] args)
{
timer.Start();
Console.WriteLine("Program Início - " + timer.Elapsed.ToString() + "\n");
Task task1Task = Task1Async();
Task task2Task = Task2Async();
await Task.WhenAll(task1Task, task2Task);
Console.WriteLine("\nProgram Fim - " + timer.Elapsed.ToString());
timer.Stop();
Console.ReadKey();
}
|
No código acima, modificamos o código para usar o método WhenAll da classe System.Threading.Tasks.Task que retorna uma Task que é concluída quando todas as tarefas em sua lista de argumentos foram concluídas.
O método Task.WhenAll é um método estático da classe Task que permite que várias tarefas assíncronas sejam executadas simultaneamente e que a conclusão de todas elas seja aguardada antes de continuar a execução do código.
Esse método recebe uma coleção de objetos Task (ou qualquer subclasse de Task, como Task<T>) como parâmetro e retorna uma nova tarefa que representa a conclusão de todas as tarefas da coleção. Ou seja, o método Task.WhenAll aguarda a conclusão de todas as tarefas passadas como parâmetro antes de concluir a tarefa retornada.
Outra opção
disponível é usar o método WhenAny de
System.Threading.Tasks.Task, que retorna uma
Task<Task> que é concluída quando qualquer um de
seus argumentos é concluído. Você pode usar este WhenAny quando tiver um cenário
como o de querer esperar apenas até que qualquer uma das tarefas seja concluída.
Depois de executar o código acima, podemos ver a saída abaixo no console:
Os resultados acima são semelhantes à versão assíncrona, ou seja, a execução
total do programa levou aproximadamente 3 segundos para ser concluída. Não há
alteração na execução do código, mas simplificamos o código usando os métodos
disponíveis na classe Task.
Até agora, abordamos como usar técnicas de programação assíncrona para melhorar
o desempenho do código C#. A seguir, vamos ver como lidar com exceções lançadas
por métodos assíncronos.
Manipulando exceções de métodos assíncronos
Os métodos assíncronos também lançam exceções da mesma forma que os métodos
síncronas. Uma Task lança exceções quando não pode
ser concluída com êxito devido a alguma exceção.
Para manipulação
de exceções, você precisa escrever o código da mesma forma que se lê como uma
série de instruções síncronas. O código de chamada pode
manipular/capturar essas exceções quando a tarefa é aguardada usando
await.
Para demonstração, vamos supor que Task1Async lance
alguma exceção de forma que não seja capaz de concluir com sucesso. Podemos
simular essa exceção modificando o código no método
Task1Async para gerar algumas exceções, conforme mostrado abaixo :
private static async Task Task1Async()
{
Console.WriteLine("Async Task 1 Iniciada - " + timer.Elapsed.TotalSeconds.ToString());
await Task.Delay(2000);
throw new Exception("Async Task 1 Falhou na execução");
//Console.WriteLine("Async Task 1 Completada - " + timer.Elapsed.TotalSeconds.ToString());
}
|
Em seguida, precisamos tratar/capturar essa exceção no método Main onde estamos aguardando a chamada para esse método assíncrono Task1Async. Para isso altere o método Main em Program.cs conforme mostrado abaixo:
public static async Task Main(string[] args)
{
timer.Start();
Console.WriteLine("Program Início - " + timer.Elapsed.ToString());
Task task1Task = Task1Async();
Task task2Task = Task2Async();
try
{
await Task.WhenAll(task1Task, task2Task);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message.ToString());
}
Console.WriteLine("Program Fim - " + timer.Elapsed.ToString());
timer.Stop();
Console.ReadKey();
}
|
Esse código aguarda (await) a conclusão de duas tarefas assíncronas (task1Task e task2Task) usando o método estático Task.WhenAll que recebe uma coleção de objetos Task (ou qualquer subclasse de Task) como parâmetro e retorna uma nova tarefa que representa a conclusão de todas as tarefas da coleção.
O uso da palavra-chave await antes da chamada do método Task.WhenAll indica que a execução do código deve ser suspensa até que todas as tarefas sejam concluídas. Quando todas as tarefas estiverem concluídas, a execução do código será retomada.
Se alguma das tarefas assíncronas gerar uma exceção durante sua execução, a execução do código será interrompida e a exceção será capturada pelo bloco catch. O bloco catch captura a exceção gerada e imprime a mensagem da exceção no console usando o método Console.WriteLine.
Depois de executar o código acima, podemos ver a saída abaixo na janela do console.
Podemos ver na saída acima que Task1Async lançou uma exceção que foi tratada pelo bloco catch do código de chamada e, consequentemente, a mensagem de exceção foi impressa no console. Podemos ver essa task whenAll esperando que todas as tarefas sejam concluídas e, em seguida, manipulamos a exceção do método Task1Async.
Com isso aprendemos o que é programação assíncrona e como usar as palavras-chave async e await no C# para implementar técnicas de programação assíncrona. Além disso, vimos como a programação assíncrona nos ajuda a melhorar o desempenho do código chamando métodos em paralelo e, assim, reduzindo o tempo geral de execução.
Continuamos no próximo artigo.
E estamos conversados...
"Então Jesus disse: "Quando vocês levantarem o Filho do homem, saberão que Eu
Sou, e que nada faço de mim mesmo, mas falo exatamente o que o Pai me ensinou."
João 8:28
Referências:
NET - Unit of Work - Padrão Unidade de ...