Introdução
Esta é uma pequena introdução às soluções utilizadas no Node.js e nos navegadores para lidar com concorrência e operações de Entrada/Saída (E/S).
A História Rápida do Node.js
Quando o Node.js foi criado, ele utilizou o conceito do Event Loop em conjunto com E/S Não Bloqueante para evitar que as operações de entrada e saída travassem o sistema.
Em praticamente todos os aplicativos web modernos, temos muita E/S:
- Interação com o banco de dados (solicitar, inserir ou atualizar registros).
- Acesso a arquivos no disco rígido.
- Comunicação em rede.
No modelo de programação síncrona, essas operações de E/S causam bloqueio, fazendo com que o programa espere (e fique inativo) até que a operação seja concluída. A solução para contornar isso é a adoção de um modelo assíncrono, implementado pelo Event Loop.
O Event Loop não foi inventado pelo pessoal do Node.js. O conceito por trás dele já existia em padrões arquiteturais como Reactor e Proactor. O Event Loop do Node.js atua como o componente central que implementa a lógica do padrão Reactor.
Entendendo o Padrão Reactor
O Padrão Reactor é um padrão de projeto de software fundamental usado em programação concorrente para lidar com eficiência com solicitações de serviço assíncronas que chegam de múltiplos clientes, utilizando um número limitado (muitas vezes, um único) de threads para a lógica principal de despacho.
O seu objetivo principal é permitir que um sistema (como um servidor de rede) gerencie múltiplas operações de E/S sem precisar de uma thread dedicada para cada conexão, melhorando a escalabilidade e o desempenho.
O padrão Reactor desacopla a detecção de que um evento está pronto (função do Desmultiplexador) do processamento real do evento (função do Manipulador).
Os principais componentes do Reactor são:
- Identificadores (Handles): Recursos de E/S que são monitorados (como um socket de rede).
-
Desmultiplexador de Eventos Síncrono: O mecanismo de nível de OS (
select(),epoll(), etc.) que bloqueia até que um ou mais identificadores estejam prontos para uma operação. - Reactor (Event Loop/Despachante): O componente principal que despacha o evento detectado para o Manipulador apropriado.
- Manipulador de Eventos (Handler): O código da aplicação que processa o evento de forma não bloqueante.
Node.js: Arquitetura de Thread Única e Múltiplas Threads
Para lidar com solicitações da Web, existem duas arquiteturas concorrentes principais: baseadas em *threads* (uma thread por requisição) e orientadas a eventos (como o Reactor).
No Node.js, o código JavaScript do usuário roda sempre em uma única *thread. Esta é a **Thread Principal* (ou Main Thread V8). É por isso que o Node.js é frequentemente descrito como "de thread única".
No entanto, para lidar com E/S de forma assíncrona, o Node.js conta com a biblioteca libuv, que é quem implementa o Event Loop.
A libuv é responsável por:
- I/O Não Bloqueante (via OS): Para a maioria das operações de rede, a
libuvusa mecanismos assíncronos nativos do sistema operacional (comoepollno Linux). Isso não envolve o Thread Pool. - I/O Bloqueante (via Thread Pool): Para operações que são inerentemente bloqueantes (como a maioria das operações de sistema de arquivos -
fs), alibuvas envia para um Pool de *Threads* (4 threads por padrão) para execução. Isso é totalmente transparente para o desenvolvedor JavaScript.
A Clarificação:
O Node.js é de thread única no que diz respeito à execução do código JavaScript.
O Node.js é multi-thread no que diz respeito à execução das tarefas de E/S pesadas, que são delegadas para o Thread Pool da
libuvpara não bloquear o Event Loop principal.
O Event Loop é o coração: A única thread de JS executa o loop de eventos. Quando uma operação de E/S (seja nativa ou do Thread Pool) termina, o callback é enfileirado e executado.
Reactor vs. Proactor: A Diferença Crucial
Ambos os padrões são modelos arquiteturais que lidam com concorrência de E/S, mas se diferenciam no momento em que o código da aplicação é invocado:
| Característica | Padrão Reactor | Padrão Proactor |
|---|---|---|
| Invocação (O quê) | Prontidão para E/S (Readiness). | Conclusão da E/S (Completion). |
| Onde a E/S ocorre | O sistema notifica que os dados estão prontos. A aplicação deve ler/escrever os dados no handle. | O sistema operacional executa a operação de E/S (leitura/escrita) em segundo plano e notifica a aplicação com o resultado pronto. |
| Modelo de I/O | Síncrono (demultiplexação) + Assíncrono (despacho). | Totalmente Assíncrono (baseado em chamadas de conclusão). |
| Exemplo (Web Servers) | Node.js (majoritariamente), Nginx. | Windows IOCP (nativamente), alguns frameworks de servidor de alta performance. |
O Caso do Node.js (libuv)
A libuv adota uma abordagem híbrida com base no sistema operacional:
- Node.js no Linux/macOS: É predominantemente Reactor.
- Node.js no Windows: Utiliza o mecanismo nativo IOCP (I/O Completion Ports), que é a base para o Proactor.
É por isso que o Node.js é frequentemente chamado de modelo Reactor + "pseudo-Proactor" via *Thread Pool*.
Analogia do Restaurante:
Padrão Reactor (O Event Loop do Node.js) O garçom recebe o pedido (tarefa de E/S). Ele é notificado de que o bife está pronto para ser cortado/finalizado. A Thread Principal (ou uma thread do Thread Pool) executa os passos finais (corta o bife/processa a E/S) e o entrega.
PROACTOR: O garçom (Thread Principal/Event Loop) recebe o pedido e o delega. Ele é notificado de que o bife está completamente pronto e já na bandeja. A Thread Principal simplesmente entrega o resultado (o sistema operacional fez todo o trabalho de E/S)
Fila de Atendimento Telefônico (Call Center):
Padrão Reactor (O Event Loop do Node.js)
O Atendente (Reactor/Event Loop): É a única thread de execução. Ele tem fones de ouvido que monitoram 100 linhas (sockets) de uma vez.
O Trabalho do OS/Desmultiplexador: Quando a Linha 5 e a Linha 12 ficam prontas (alguém começou a falar/dados prontos), o sistema notifica o atendente.
Ação da Aplicação: O atendente para de monitorar momentaneamente e ouve a Linha 5 (lê os dados), processa o pedido rapidamente e desliga (executa o callback). O atendente não fica esperando na linha 5; ele só reage quando a linha está pronta.
Padrão Proactor
O Cliente (Aplicação): O cliente diz ao sistema telefônico: "Quando esta pessoa terminar de me dar todas as 5 informações que preciso, ligue-me de volta."
O Trabalho do OS/Sistema: O sistema (em segundo plano, com suas próprias threads) completa toda a interação (pega as 5 informações) e só então notifica a aplicação principal.
A Ação da Aplicação: A aplicação principal recebe uma notificação de conclusão que já contém todas as 5 informações.
REACTOR: "Avise-me quando puder fazer o trabalho." (Ênfase na prontidão.)
PROACTOR: "Avise-me quando o trabalho estiver concluído." (Ênfase na conclusão.)
As Fases do Event Loop do Node.js
O Event Loop divide seu ciclo de execução em fases, girando sobre o modelo Reactor para distribuir eventos:
- Timers: Execução de callbacks de
setTimeoutesetInterval. - Pending Callbacks: Execução de callbacks de operações internas pós-E/S.
- Idle, Prepare: Uso interno da
libuv. - Poll (Sonda): Esta é a fase onde o Reactor realmente acontece.
- O
libuvchama o Desmultiplexador de Eventos (ex:epoll_wait()). - Ele espera que sockets fiquem prontos (
read-ready/write-ready). - Ao ser notificado, ele acorda e despacha os eventos para os handlers registrados.
- O
- Check: Execução de callbacks de
setImmediate. - Close Callbacks: Execução de callbacks de fechamento (ex:
socket.on('close')).
Conclusão: O ciclo inteiro é o Reactor (o despachante) rodando e distribuindo eventos. Mas a fase Poll é a parte que implementa o Desmultiplexador de Eventos Síncrono (a espera pela prontidão da E/S) do Padrão Reactor.
Em resumo, o sucesso do Node.js reside na adoção do padrão Reactor (e uma implementação híbrida com o Proactor em certas plataformas), garantindo que a Thread Principal do JavaScript nunca bloqueie, permitindo uma escalabilidade enorme com baixo consumo de memória.
Fontes para aprofundamento do conteúdo!
https://betterprogramming.pub/scalable-concurrency-meet-non-blocking-i-o-edb6b39c59d7
http://didawiki.cli.di.unipi.it/lib/exe/fetch.php/magistraleinformatica/tdp/tpd_reactor_proactor.pdf
https://en.wikipedia.org/wiki/Reactor_pattern
https://en.wikipedia.org/wiki/Proactor_pattern
https://tusharf5.com/posts/node-design-patterns-reactor-pattern/
https://progressivecoder.com/nodejs-reactor-pattern-event-loop/
https://medium.com/@mohamed-khattab/demystifying-the-reactor-design-pattern-in-node-js-a8aabd73a315
https://www.youtube.com/watch?v=Vm5l8zH4hOE
https://www.artima.com/articles/comparing-two-high-performance-io-design-patterns
Top comments (0)