logo

Banner do Post

Programação assíncrona, Event Loop e NodeJs

A programação assíncrona é um recurso presente em diversas linguagens de programação. Porém, o NodeJs, que é a tecnologia usada para executar código JavaScript, usa esse recurso de uma forma bastante peculiar por possuir um modelo de concorrência baseado em um Event Loop. Nesse artigo, você irá compreender um pouco sobre cada um desses assuntos e qual é a relação entre eles.

Introdução

A maioria dos back-ends por trás dos sites mais famosos não fazem computações complicadas. Esses programas passam a maior parte do tempo lendo ou escrevendo no disco, ou melhor, esperando a sua vez de ler e escrever, uma vez que é um recurso lento e concorrido. Quando não estão nesse processo de ir ao disco, estão enviando ou recebendo bytes da rede, que é outro processo igualmente demorado. Ambos processos podemos resumir como operações de I/O (Input & Output) ou E/S (Entrada & Saída).

Processar dados, ou seja executar algoritmos, é infinitamente mais rápido do que qualquer operação de IO que um software possa fazer. Mesmo se tivermos um SSD em nossa máquina com velocidades de leitura de 200-730 MB/s fará com que a leitura de 1KB de dados leve 1.4 microssegundos. Parece rápido? Saiba que nesse tempo uma CPU de 2GHz consegue executar 28.000 instruções. Quando falamos de IO de rede é ainda pior, o tempo de resposta pode ser tão grande que a CPU poderia está executando 88 milhões de operações ao invés de aguardar uma ser executada.

A programação assíncrona

Normalmente, o código de um programa é executado de forma direta, com uma coisa acontecendo por vez. Se uma função depende do resultado de outra função, ela tem que esperar o retorno do resultado, e até que isso aconteça, o programa inteiro praticamente para de funcionar da perspectiva do usuário.

Por exemplo, quando o usuário de Windows tenta executar alguma tarefa do sistema e o cursor muda para uma ampulheta. Este cursor é o jeito do sistema operacional dizer: "o programa atual que você está usando teve que parar e esperar algo terminar de ser executado, e estava demorando tanto que mudei o cursor para que você espere."

Essa é uma situação comum, e que alguns podem dizer que não faz bom uso do poder de processamento do computador — especialmente em uma era em que computadores tem múltiplos núcleos de processamento disponíveis. Não há sentido em ficar esperando por algo quando você pode deixar outra tarefa ser executada em um núcleo de processador diferente e deixar que ele te avise quando terminar. Isso te permite fazer mais coisas por enquanto, o que é a base da programação assincrona. Então utilizar todas as threads que o sistema tem para oferecer seria melhor ? não exatamente.

NodeJs e o Event Loop

Em um servidor web utilizando linguagens tradicionais, para cada requisição recebida é criada uma nova thread para tratá-la. A cada requisição, serão demandados recursos como, memória RAM, para a criação dessa nova thread. Uma vez que esses recursos são limitados, as threads não serão criadas infinitamente, e quando esse limite for atingido, as novas requisições terão que esperar a liberação desses recursos alocados para serem tratadas.

Já o NodeJs é uma tecnologia assíncrona que trabalha em uma única thread de execução. Ou seja, cada requisição ao NodeJs não bloqueia o processo do mesmo, atendendo a um volume absurdamente grande de requisições ao mesmo tempo mesmo sendo single-threaded. E essa thread é chamada de Event Loop, e leva esse nome pois cada requisição é tratada como um evento, e o Event Loop fica em execução esperando novos eventos para tratar, e para cada requisição, um novo evento é criado.

Apesar de ser single-threaded, o NodeJs consegue tranquilamente tratar requisições concorrentes. Enquanto o servidor tradicional utiliza o sistema multi-thread para tratar essas requisições, o NodeJs consegue o mesmo efeito através de chamadas de E/S (entrada e saída) não-bloqueantes ou Non-blocking I/O. Isso significa que as operações de entrada e saída (ex: acesso a banco de dados e leitura de arquivos do sistema) são assíncronas e não bloqueiam a thread. Diferentemente dos servidores tradicionais, a thread não fica esperando que essas operações sejam concluídas para continuar sua execução.

Entendendo o Event Loop

Nesse contexto, você já deve ter percebido que o Event Loop é um loop simultâneo de thread única, sem bloqueio e de forma assíncrona. Imagine uma solicitação web que faz uma pesquisa no banco de dados. Sabemos que uma única thread só pode fazer uma coisa de cada vez e que em vez de aguardar a resposta do banco de dados, o NodeJs continua a selecionar outras tarefas na fila. No Event Loop, o loop principal desenrola a pilha de chamadas (Callstack) e não espera os retornos de chamada. Como o loop é Non-blocking I/O, é tranquilo fazer mais de uma solicitação web por vez. Várias solicitações podem ser enfileiradas ao mesmo tempo, o que as torna simultâneas. Desse modo, não é necessário esperar que uma requisição seja concluída e quando ela enviar um retorno a resposta é utilizada pelo sistema.

"Qual é a vantagem de executar tudo em uma única thread?"

Sabemos que as threads são recursos limitados do sistema e se uma thread depender de outra para executar uma tarefa específica o comportamento vai ser o mesmo de um ambiente single-thread, portanto a flexibilidade e a leveza que a arquitetura single-threaded que o NodeJs tem, o torna extremamente escalável.

Para ilustrar melhor como essa arquitetura funciona vamos voltar ao exemplo do banco de dados, imagine o recebimento de uma grande quantidade de dados simultâneos em uma aplicação, como o instagram, isso pode criar gargalos no banco de dados devido à alta demanda por capacidade de resposta (a confirmação de gravação dos dados, por exemplo).

Para suportar esse grande volume de solicitações e não criar operações de bloqueio , é preciso usar um modelo que não demande resposta. Além de funcionar com a single-threaded sem bloqueio (Non-blocking I/O), o NodeJs agiliza e facilita as conexões criadas para a visualização de informações, uma vez que opera em JSON. Dessa forma, é ideal para:

  • registrar ou gravar dados de rastreamento de usuários;
  • processar lotes de informações que podem ser usadas posteriormente;
  • operar atualizações que não precisam ser refletidas instantaneamente (a contagem de curtidas, por exemplo).

Dessa forma, os dados são enfileirados por meio de algum tipo de infraestrutura de armazenamento em cache, como RabbitMQ ou ZeroMQ, e processados em lote separadamente, por meio de hardwares diferentes.

Um outro bom exemplo é a facilidade de empregar o NodeJs como um proxy do lado do servidor, por suportar uma grande quantidade de conexões simultâneas sem bloqueio. Isso é útil em proxies de diferentes serviços e com tempos de resposta variados. Também servindo para coletar dados de vários pontos de origem: podendo ser usado em um aplicativo que extrai informações de diferentes fontes, como imagens e vídeos, para fazer a compilação depois, por exemplo. Embora existam servidores proxy dedicados, o NodeJs pode transformar uma infraestrutura comum em proxy de base local.

Recapitulando

No servidor NodeJs, o Event Loop é a única thread que trata as requisições, enquanto que no modelo tradicional uma nova thread é criada para cada requisição. Enquanto o Event Loop delega uma operação de I/O para uma thread do sistema de forma assíncrona e continua tratando as outras requisições que aparecerem em sua pilha de eventos, as threads do modelo tradicional esperam a conclusão das operações de I/O, consumindo recursos computacionais durante todo esse período de espera.

Apesar do NodeJs ser single-threaded, sua arquitetura possibilita um número maior de requisições concorrentes sejam tratadas em comparação com o modelo tradicional, que é limitado devido ao alto consumo computacional pela criação e manutenção de threads a cada requisição.

Bonus: Vantagens de uso do NodeJs

  • Flexibilidade

O NPM (Node Package Manager) é o gerenciador de pacotes do NodeJs e também é o maior repositório de softwares do mundo. Isso faz do NodeJs uma plataforma com potencial para ser utilizada em qualquer situação. O pacote mais conhecido se chama Express.js e é um framework completo para desenvolvimento de aplicações Web.

  • Leveza

Criar um ambiente NodeJs e subir uma aplicação é uma tarefa que não exige muitos recursos computacionais em comparação com outras tecnologias mais tradicionais. Se utilizado em conjunto com ferramentas como o Docker, o ganho na velocidade de deploy e replicação de máquinas pode ser muito significativo e em ambientes escaláveis isso significa menos custo e mais eficiência.

Tanto sua leveza quanto flexibilidade fazem do NodeJs uma tecnologia indicada para a implementação de serviços e componentes de arquiteturas como a de microsserviços e serverless. Além disso, conta com suporte das principais empresas de produtos e serviços Cloud do mercado, como a AWS, Google Cloud e Microsoft Azure que oferecem na maioria de seus produtos suporte nativo ao NodeJs.

  • Produtividade da equipe

Maior repositório do mundo: O NPM fornece pacotes de código reusáveis e provavelmente aquela integração que você precisa fazer com outro sistema ou banco de dados já está implementado e disponível gratuitamente para instalar via NPM.

Mesma linguagem no frontend e backend: JavaScript é a linguagem padrão para desenvolvimento web client-side. Empresas de desenvolvimento Web contam como esse know-how como um ponto de partida importante para iniciar o uso do NodeJs. Além disso, esse fator pode representar ganhos de reutilização de código e criação de equipes multidisciplinares, com melhor aproveitamento de recursos.

Ambiente de inovação: Possibilidade de deploys e iterações mais rápidas, e resolução de problemas On the Fly. Isso também permite a criação de soluções próprias e inovadoras.

Bonus: Casos de uso mais comuns

  • Aplicações em Tempo Real

Como já foi mencionado no tópico sobre "Qual é a vantagem de executar tudo em uma única thread?", um exemplo comum é uma aplicação de conversa (chat). Tal aplicação exige muito pouco processamento e basicamente consiste em transferir as mensagens de um lado para outro.

  • Ambientes Escaláveis

O NodeJs é bastante indicado para ambientes escaláveis (com grande número de conexões concorrentes), já que tem potencial para suportar um número maior de conexões simultâneas do que servidores tradicionais.

  • Camada de Entrada do Servidor

O NodeJs faz pouco processamento de dados e apenas passa a requisição para frente, se comunicando com serviços de back-end.

  • Mocks e Protótipos

Por utilizar uma linguagem bastante conhecida e uma das mais utilizadas no mundo, o NodeJs possibilita criar mocks e protótipos de APIs e serviços de back-end com grande rapidez, podendo assim simular a comunicação com um serviço externo, por exemplo.

  • API com NoSQL por trás

As base de dados NoSQL são baseadas em JSON (JavaScript Object Notation), portanto, sua comunicação com NodeJs é bastante intuitiva. Portanto, não é necessário converter modelos de dados, pois os mesmos objetos JavaScript armazenados na base de dados, podem ser enviados para o front-end sem a necessidade de nenhum tipo de tratamento ou conversão.

Conclusão

Nesse artigo você pôde de ter uma noção ampla sobre como a programação assíncrona, Event Loop e NodeJs estão interligados e um pouco sobre como tudo isso funciona por "de baixo dos panos". E também lhe apresentei como essas ferramentas estão tornando o mundo do desenvolvimento cada vez mais inovador. Claro que ainda existem muito mais informações sobre esses assuntos como: Asyn/Await, Multi threading, Microtasks Macrotasks. Talvez futuramente eu aprofunde em algum deles por aqui. Qualquer dúvida, dicas ou sugestões, não se esqueça de deixar nos comentário aqui em baixo.

Espero que tenha curtido 🤟

Construindo interfaces declarativas com Chakra UI

Post anterior

Qual é a diferença entre "==" e "===" no JavaScript ?

Próximo post