Resposta simples: Node.js é single-threaded e divide esse único thread para simular concorrência, enquanto Elixir aproveita a concorrência e o paralelismo, nativo, da BEAM, a máquina virtual do Erlang, para executar processos simultaneamente.
Abaixo, vamos entender mais a fundo essa diferença, explorando dois conceitos-chave: o event loop do Node.js e a BEAM VM e OTP do Elixir. Esses elementos são cruciais para compreender como cada tecnologia lida com a execução de tarefas assíncronas e como isso afeta o desempenho e a escalabilidade em diferentes aplicações.
1. O Que é o Event Loop?
O Node.js opera em uma única thread principal e utiliza um mecanismo chamado event loop para gerenciar operações assíncronas. O conceito básico é que ele verifica se há tarefas pendentes para serem processadas, como operações de I/O, promises e callbacls, e as executa quando estão prontas.
1.1 Como funciona na prática:
Quando uma operação assíncrona é iniciada (por exemplo, uma consulta a uma API por exemplo), ela é delegada para a libuv. Enquanto isso, o event loop continua a aceitar outras conexões.
Quando a operação assíncrona termina, a libuv retorna o resultado para a event queue, então event loop coloca o callback associado à operação, na call stack.
1.2 Limitações do Event Loop:
Se uma tarefa demorada ou de CPU intensa estiver na call stack, ela pode bloquear o processamento de outras operações, reduzindo a eficiência.
A concorrência é limitada, pois tudo é executado em um único thread principal.
2. BEAM VM e OTP
Elixir é construído sobre a BEAM VM, a mesma máquina virtual que alimenta Erlang, conhecida por sua capacidade de lidar com alta concorrência e resiliência. Ao contrário de Node.js, Elixir não depende de um único thread. Em vez disso, utiliza processos extremamente leves e isolados gerenciados pela BEAM.
2.1 Como funciona na prática:
- Cada processo em Elixir é independente, o que significa que eles não compartilham memória e não bloqueiam uns aos outros.
- Esses processos são gerenciados pela BEAM, que pode criar e gerenciar milhões de processos simultaneamente, distribuindo a carga entre todos os núcleos de CPU disponíveis.
- Além disso, Elixir vem com o OTP (Open Telecom Platform), que fornece um conjunto de bibliotecas e ferramentas para construir sistemas robustos e distribuídos.
2.2 Vantagens da BEAM e OTP:
- Escalabilidade: A BEAM pode distribuir processos entre todos os núcleos de CPU, maximizando o uso de recursos.
- Resiliência: Se um processo falha, ele não afeta outros processos. Isso permite construir sistemas tolerantes a falhas.
- Concorrência Real: Diferente do loop de eventos, que é limitado por um único thread, Elixir pode executar processos verdadeiramente em paralelo, aproveitando múltiplos núcleos de CPU.
3. Comparando Node.js e Elixir na Prática
Vamos imaginar um servidor que precisa lidar com milhares de conexões simultâneas, cada uma realizando operações asyncronas e alguns processamentos pesado e mais demorados.
3.1 Com Node.js:
- O servidor é eficiente até certo ponto, mas à medida que as operações pesadas se acumulam, o loop de eventos começa a ficar sobrecarregado. Embora um bom uso dos recursos disponivel no JS e Node podem ajudar bastante na performance: como o uso correto do
async/wait
e/outhen/catch
e recursos built-in como a libnode:cluster
- Isso pode levar a atrasos nas resposta de novas conexões, resultando em um empacto significativo no desempenho.
3.2 Com Elixir:
- Cada conexão pode ser gerenciada por um processo separado. Operações de I/O, computação e até falhas podem ser geridas de forma isolada.
- A BEAM distribui a carga de forma eficiente, garantindo que o sistema continue a funcionar sem maiores problemas, mesmo sob alta demanda.
- Se necessario é possivel fazer uma comunicacao entre processos via mensagem.
- Mecanismo Preemptive Scheduling da BEAM.
Conclusão
Node.js é uma excelente ferramenta para muitas aplicações, especialmente aquelas que lidam com operações assíncronas simples e que não exigem processamento pesado de CPU. No entanto, seu modelo de concorrência baseado em um único thread pode ser um gargalo em cenários mais complexos.
Elixir, com BEAM VM e suporte nativo a processos leves e concorrência massiva, oferece uma alternativa robusta e eficiente para sistemas que precisam lidar com grande número de operações simultâneas e distribuir carga entre múltiplos threads do CPU. Se você precisa de resiliência, escalabilidade e alta concorrência, Elixir é a escolha.
Embora o título deste artigo seja ousado ao sugerir que Elixir e a BEAM superam Node.js em processamento assíncrono, é importante reconhecer que existem diferenças significativas entre essas tecnologias. A decisão sobre qual delas utilizar deve considerar uma variedade de fatores, não apenas a concorrência e o paralelismo abordados aqui. Aspectos como o ecossistema, a familiaridade da equipe com a linguagem, requisitos específicos do projeto, e a natureza das tarefas a serem executadas desempenham um papel crucial na escolha da melhor ferramenta para o trabalho. Afinal, cada cenário tem suas particularidades, e a escolha da tecnologia deve ser feita com uma visão holística, levando em conta todas as necessidades e desafios do projeto.
Bibliografia
Threads:
Threads são as menores unidades de execução em um programa. Em muitos sistemas operacionais, um processo pode conter múltiplas threads, cada uma executando uma parte diferente do programa. Threads podem compartilhar memória e recursos, mas isso pode levar a problemas de concorrência, como condições de corrida.
Concorrência:
Concorrência é a capacidade de um sistema lidar com múltiplas tarefas ao mesmo tempo. Em um sistema concorrente, várias tarefas podem progredir independentemente, mesmo que não estejam sendo executadas simultaneamente. A BEAM, por exemplo, gerencia processos concorrentes que operam de forma independente.
Event Loop:
O event loop é um padrão de design usado em sistemas como Node.js para gerenciar operações assíncronas. Ele funciona em um único thread, executando tarefas de forma cíclica, respondendo a eventos como I/O e execuções assíncronas, garantindo que o programa continue respondendo enquanto espera por operações longas.
Paralelismo:
Paralelismo é a execução simultânea de múltiplas tarefas em diferentes núcleos de CPU. Diferente da concorrência, que se refere à gestão de tarefas simultâneas, o paralelismo envolve a execução real dessas tarefas ao mesmo tempo. A BEAM distribui processos em múltiplos núcleos para maximizar o paralelismo.
Processos Leves (Lightweight Processes):
Na BEAM, processos leves são unidades de execução que são muito mais eficientes em termos de memória e CPU do que threads tradicionais. Eles são isolados uns dos outros e gerenciados pela BEAM, o que permite criar e gerenciar milhões de processos simultâneos.
Preemptive Scheduling:
Preemptive scheduling é um sistema de gerenciamento de tempo de execução onde o sistema operacional ou a máquina virtual atribui fatias de tempo a cada processo, garantindo que nenhum processo monopolize a CPU. Na BEAM, isso assegura que todos os processos tenham a chance de ser executados de maneira justa.
BEAM VM:
A BEAM (Bogdan/Björn's Erlang Abstract Machine) é a máquina virtual que executa código Erlang e Elixir. É conhecida por sua habilidade em gerenciar processos leves de forma eficiente, suportando concorrência massiva e paralelismo, além de fornecer tolerância a falhas.
OTP (Open Telecom Platform):
OTP é um conjunto de bibliotecas e padrões de design que acompanham Erlang e Elixir. Ele fornece ferramentas para construir sistemas concorrentes, distribuídos e tolerantes a falhas, facilitando o desenvolvimento de aplicações robustas e escaláveis.
libuv
é uma biblioteca multi-plataforma que fornece suporte para operações de I/O assíncronas em Node.js. Ela é responsável por implementar o event loop e abstrair funcionalidades de sistema operacional, como operações de rede, sistema de arquivos, e threads. libuv permite que Node.js execute tarefas assíncronas de maneira eficiente em um único thread, utilizando um pool de threads internas para operações bloqueantes, garantindo a continuidade do event loop principal.
operações de I/O
Operações de I/O (Input/Output) referem-se a qualquer interação entre um programa e o mundo externo, como ler ou escrever em arquivos, comunicar-se com dispositivos de hardware, ou trocar dados pela rede. Essas operações podem ser demoradas e, em muitos sistemas, são realizadas de maneira assíncrona para evitar que o programa fique bloqueado enquanto aguarda a conclusão da operação.
Referências
ERLANG. A brief BEAM primer. Erlang Blog, 2020. Disponível em: https://www.erlang.org/blog/a-brief-beam-primer/. Acesso em: 29 ago. 2024.
ERLANG. Getting started with Erlang [PDF]. Erlang.org. Disponível em: https://erlang.org/download/erlang-book-part1.pdf. Acesso em: 29 ago. 2024.
NODE.DOCTORS. An animated guide to Node.js event loop. Dev.to, 2021. Disponível em: https://dev.to/nodedoctors/an-animated-guide-to-nodejs-event-loop-3g62. Acesso em: 29 ago. 2024.
NODE.DOCTORS. Animated Node.js event loop phases. Dev.to, 2022. Disponível em: https://dev.to/nodedoctors/animated-nodejs-event-loop-phases-1mcp. Acesso em: 29 ago. 2024.
NODE.JS. Cluster. Node.js, 2023. Disponível em: https://nodejs.org/api/cluster.html. Acesso em: 29 ago. 2024.