C# -  Programação Assíncrona - I


  Vamos recordar conceitos importantes da programação assíncrona na linguagem C#

Neste artigo, vamos recordar os conceitos da programação assíncrona com exemplos de código, ou seja, o que é programação assíncrona, como ela é diferente da programação síncrona, como implementá-la no .NET Core usando async & await, quando e onde ela deve ser usada junto com onde é melhor evitar o seu uso.

Nos dias de hoje, considerando a natureza dos aplicativos, sua carga e a demanda constante dos clientes para melhorar a escalabilidade do aplicativo, torna-se muito importante entender e implementar adequadamente os conceitos de programação assíncrona em seu aplicativo.

Se você tiver alguma necessidade de realizar operações de I/O (como solicitar dados de uma rede, acessar um banco de dados ou ler e gravar em um sistema de arquivos), você desejará utilizar a programação assíncrona. Você também pode ter código vinculado à CPU, como executar um cálculo complexo e demorado, que também é um bom cenário para escrever código assíncrono.

O C# possui um modelo de programação assíncrona em nível de linguagem, que permite escrever facilmente código assíncrono sem ter que fazer malabarismos com retornos de chamada ou estar em conformidade com uma biblioteca que oferece suporte à assincronia. Ela segue o que é conhecido como Padrão Assíncrono Baseado em Tarefas (TAP).

Como muitas outras linguagens de programação, o C# também oferece suporte à programação assíncrona e  quando se trata de programação assíncrona no .NET, existem 2 palavras-chave muito importantes : async e await.

O núcleo da programação assíncrona no .NET são os objetos Task e Task<T>, que modelam operações assíncronas. Eles são suportados pelas palavras-chave async e await.

  1. Para código vinculado a I/O, você aguarda(await) uma operação que retorna uma Task ou Task<T> dentro de um método assíncrono;
     
  2. Para código vinculado à CPU, você aguarda (await) uma operação iniciada em uma thread em segundo plano com o método Task.Run;

A palavra-chave await é onde a mágica acontece. Ela cede o controle ao chamador do método que executou o await e, em última análise, permite que uma interface do usuário seja responsiva ou um serviço seja elástico. Embora existam maneiras de abordar o código assíncrono diferente de async e await, vamos focar neste recurso.

Os códigos mostrados neste artigo, foram criados usando o C# 11 e o Visual Studio Community 2022 (17.5.1) no ambiente do .NET 7.0.

Programação Assíncrona x Programação Paralela

A programação assíncrona e a programação paralela são duas técnicas diferentes para melhorar o desempenho de aplicativos e aproveitar melhor os recursos do sistema. No C#, ambas as técnicas são suportadas nativamente e oferecem recursos poderosos para lidar com tarefas que exigem muito processamento.

A programação assíncrona é um modelo de programação que permite que o aplicativo execute várias tarefas ao mesmo tempo sem bloquear a thread principal. Isso é feito usando a palavra-chave "async" e a biblioteca de tarefas TPL (Task Parallel Library) do .NET. Com a programação assíncrona, o aplicativo pode continuar executando outras tarefas enquanto aguarda a conclusão de uma operação de I/O ou de um cálculo complexo em outra thread. Isso pode melhorar significativamente a capacidade de resposta do aplicativo e a utilização do processador.

A programação paralela, por outro lado, é um modelo de programação que permite que o aplicativo execute várias tarefas ao mesmo tempo, aproveitando os recursos do processador. Isso é feito usando a biblioteca Parallel do .NET, que fornece vários métodos para executar operações em paralelo. Com a programação paralela, o aplicativo pode dividir uma tarefa em várias partes menores e executá-las simultaneamente em várias threads ou núcleos de processador. Isso pode acelerar a execução da tarefa e reduzir o tempo de resposta do aplicativo.

Programação Assíncrona x Programação Síncrona

A programação síncrona é um estilo de programação em que cada ação é executada de forma sequencial e bloqueante, ou seja, a execução do programa fica parada até que uma determinada tarefa seja concluída antes de seguir para a próxima tarefa. Nesse modelo, é necessário esperar a conclusão de uma tarefa para iniciar a próxima.

Já na programação assíncrona, as tarefas são executadas em paralelo, sem bloquear a execução do programa. Isso significa que, em vez de aguardar a conclusão de uma tarefa antes de iniciar a próxima, é possível iniciar várias tarefas ao mesmo tempo e esperar pela conclusão de cada uma delas.

No C#, o suporte à programação assíncrona foi adicionado na versão 5.0 da linguagem. Uma das principais formas de se trabalhar com programação assíncrona em C# é por meio do uso de palavras-chave como "async" e "await".

Ao se utilizar essas palavras-chave, é possível criar métodos que executam tarefas assincronamente, ou seja, sem bloquear a execução do programa. Quando um método é marcado como "async", ele pode ser interrompido durante a sua execução, permitindo que outras tarefas sejam executadas em paralelo.

O uso da programação assíncrona é indicado para situações em que a execução de uma tarefa pode levar muito tempo ou quando há a necessidade de se realizar várias tarefas simultaneamente. Por outro lado, a programação síncrona é mais indicada para situações em que a execução das tarefas deve ser feita de forma sequencial, uma após a outra, ou quando não há a necessidade de se realizar tarefas simultaneamente

Programação Assíncrona

A programação Assíncrona permite que você execute a tarefa em segundo plano, de forma que o processo ou thread não esteja ocupado ou bloqueado. Ela também permite executar duas ou mais tarefas independentes em paralelo. Este modo non-blocking e paralelização permitem evitar ou reduzir os atrasos devido a esperas/bloqueios que ocorrem durante a execução do código.

Nem todas as tarefas podem ser executadas em paralelo, pois algumas tarefas podem depender do resultado de outra tarefa, ou seja, pode haver um cenário como esse, precisamos executar a tarefa 2 somente se a tarefa 1 for bem-sucedida, nesse caso, teremos que aguardar a tarefa 1 para concluir e, em seguida, com base no resultado da tarefa 1, precisamos decidir se podemos executar a tarefa 2 ou não.

A programação assíncrona é usada não apenas para melhorar o desempenho ou a escalabilidade de seu aplicativo, mas também para melhorar a experiência geral do usuário com o aplicativo.

No C#, usamos as palavras-chave async e await para implementar a programação assíncrona. Para qualquer método ser assíncrono, temos que adicionar a palavra-chave async na definição do método antes do tipo de retorno do método. Além disso, é prática geral adicionar Async ao nome do método se esse método for assíncrono.

public async Task SaveDataAsync()
{
  
//Salvar Dados
}

Tipos de retorno na programação assíncrona

Os tipos de retorno assíncronos ou Async Return Types são os tipos de retorno assíncronos disponíveis no C# para representar o resultado de uma operação assíncrona. Eles permitem que uma operação assíncrona possa retornar um valor e, ao mesmo tempo, ser executada de forma não sequencial e não bloqueante.

Existem os seguintes tipos de retorno assíncronos no C#:

  1. Task: É usada para representar uma tarefa assíncrona que não retorna um valor. É o tipo de retorno padrão para uma operação assíncrona que não precisa retornar um valor.
     
    public async Task SaveDataAsync()
    {
      
    //Salvar Dados
    }
  2. Task<T>: É usada para representar uma tarefa assíncrona que retorna um valor do tipo T. É o tipo de retorno padrão para uma operação assíncrona que precisa retornar um valor.
     
    public async Task<int> SaveDataAsync()
    {
      
    //Salvar Dados
       return 1;
    }
  3. ValueTask<T>:  É uma alternativa mais eficiente à classe Task<T> que também é usada para representar uma tarefa assíncrona que retorna um valor do tipo T. A diferença é que a classe ValueTask<T> é uma estrutura de valor (struct), que é mais eficiente em algumas situações em que a alocação de objetos é um problema.
     
    public async ValueTask<int> SaveDataAsync()
    {
      
    //Salvar Dados
      
    return 1;
    }
  4. void : O tipo de retorno void pode ser usado em manipuladores de eventos assíncronos que requerem um tipo de retorno void. Para métodos assíncronos que não retornam um valor, use Task em vez de void, pois métodos assíncronos que retornam void não podem ser aguardados (awaited). Nesse caso, o chamador irá disparar e esquer o método,e além disso, o chamador de um método assíncrono que retorna void não pode capturar as exceções geradas pelo método.
     
    public async void SaveDataAsync()
    {
      
    //Salvar Dados
    }
  5. IAsyncEnumerable<T>: O tipo de retorno IAsyncEnumerable<T> é um recurso introduzido no C# 8.0 que permite a iteração assíncrona de uma sequência de valores de forma eficiente. Ele é usado para retornar uma sequência de resultados assíncronos de uma operação assíncrona. Ele permite que o consumidor da sequência comece a receber os resultados assim que o primeiro valor estiver disponível, sem ter que esperar a conclusão completa da operação. Isso significa que a operação assíncrona pode começar a produzir resultados assim que eles estiverem prontos, em vez de esperar até que todos os resultados estejam prontos para serem retornados.

    O uso de IAsyncEnumerable<T> é especialmente útil para operações assíncronas que envolvem a leitura ou a escrita de grandes quantidades de dados, como o acesso a bancos de dados ou a leitura de arquivos grandes. Ele ajuda a evitar a sobrecarga de memória, permitindo que o consumidor processe os resultados à medida que são produzidos.

    A classe IAsyncEnumerable<T> é uma interface genérica que define métodos para permitir a iteração assíncrona de uma sequência de valores. Ela é implementada por classes como AsyncEnumerable e AsyncStream, que podem ser usadas para criar e retornar sequências de resultados assíncronos.

A escolha do tipo de retorno adequado depende do tipo de operação assíncrona que está sendo executada. Se a operação não precisar retornar um valor, o tipo Task é suficiente. Se a operação precisar retornar um valor, o tipo Task<T> é o mais apropriado. Já a classe ValueTask<T> é uma opção interessante quando há um grande número de chamadas assíncronas sendo realizadas e a alocação de objetos se torna um problema.

Vantagens, desvantagens e quando usar a programação assíncrona

A seguir temos os principais benefícios da programação assíncrona:

Maior responsividade – Ao executar tarefas de longa duração em segundo plano, podemos manter a interface do usuário no modo sem bloqueio para que os usuários possam interagir com os aplicativos e realizar outras operações.

Melhoria de desempenho – Podemos melhorar o desempenho de nosso aplicativo executando tarefas independentes em paralelo e, se duas ou mais tarefas forem executadas em paralelo, levará menos tempo para a execução da atividade.

Melhor desempenho: A programação assíncrona permite que o código execute várias tarefas em paralelo, aproveitando ao máximo os recursos do sistema. Isso pode resultar em um desempenho significativamente melhor do que a programação sequencial.

Melhor Escalabilidade: A programação assíncrona permite que um sistema seja escalonado para suportar mais usuários e operações, sem a necessidade de adicionar mais recursos de hardware. Isso é particularmente importante em sistemas que lidam com grande quantidade de solicitações simultâneas.

Embora haja benefícios claros da programação assíncrona, ela aumenta a complexidade durante o desenvolvimento de aplicativos e isso torna os aplicativos assíncronos muito mais complexos, tornando difícil aprimorar ou modificar a funcionalidade do aplicativo. Também é difícil depurar e encontrar bugs em aplicativos assíncronos. Como há várias tarefas em execução em paralelo, aumenta o número de objetos na memória e também, em alguns casos, esses objetos precisam ficar mais tempo na memória até que todas as tarefas sejam concluídas.

A programação assíncrona pode ser usada para a tarefa de bloqueio de execução longa. Sempre que houver um código de bloqueio que pode ser executado independentemente do restante do processo, nesse caso, podemos executar esse código como uma tarefa assíncrona. Esse modo de tarefa assíncrona executará esse trecho de código em uma thread diferente em vez da thread principal e a thread principal pode ficar livre para manter o aplicativo no estado de resposta.

Exemplos de tarefas de longa execução na programação podem ser:   ler ou gravar um arquivo, chamadas HTTP para uma API externa de terceiros, fazer chamadas de banco de dados pela rede, realizar cálculos pesados nos dados, gravar logs de aplicativos em um arquivo, etc.

O que acontece debaixo das cobertas

No lado C# das coisas, o compilador transforma seu código em uma máquina de estado que monitora coisas como ceder a execução quando uma espera é alcançada e retomar a execução quando uma tarefa em segundo plano é concluída.

Pontos chaves a compreender :

Após toda essa teoria, indigeta mas necessária, vamos arregaçar as mangas e por tudo isso em prática mostrando agora exemplos de uso da programação assíncrona na linguagem C#.

Na próxima parte do artigo vamos iniciar a parte prática.

E estamos conversados...

"Todas as coisas são puras para os puros, mas nada é puro para os contaminados e infiéis; antes o seu entendimento e consciência estão contaminados."
Tito 1:15

Referências:


José Carlos Macoratti