Enquanto escrevo meu livro Construindo APIs testáveis com Node.js acabei fazendo uma imersão no código do google v8 e também no Node.js para entender como eles trabalham juntos. Agora resolvi dividir esse aprendizado com vocês, esse conteúdo também estará no livro, então todo o feedback é muito bem vindo. O conteúdo se divide em duas partes, a primeira esta aqui , ela é mais introdutória a o que é o google v8, i/o assíncrono e single thread. Aconselho fortemente a leitura antes de prosseguir no artigo de hoje.
A foto a seguir ilustra minha situação quando resolvi transcrever essa estrutura para algo visual, o mesmo que vamos ver no artigo:
Conclusão: Não é tão simples como dizem hahaha
Event Loop
O Node.js é guiado por eventos, termo também conhecido como Event Driven, esse conceito já é bastante aplicado em interações com interface de usuário. O javascript possui diversas APIs baseadas em eventos para interações com o DOM por exemplo, eventos como onClick, onHide, onShow são muito comuns no mundo front-end com javascript.
Event driven é um fluxo de controle determinado por eventos ou alterações de estado, a maioria das implementações possuem um core (central) que escuta todos os eventos e chama seus respectivos callbacks quando eles são lançados (ou tem seu estado alterado). Esse basicamente é o resumo do Event Loop do Node.js.
Separadamente a responsabilidade do Event Loop parece simples, mas quando nos aprofundamos para entender como o Node.js trabalha, notamos que o Event Loop é a peça chave para o sucesso do modelo event driven. Nos tópicos seguintes iremos entender cada um dos componentes que formam o ambiente do Node.js, como eles funcionam e como se conectam.
Call Stack
A stack (pilha) é um conceito bem comum no mundo das linguagens de programação, frequentemente se ouve algo do tipo: “Estourou a pilha!”. No Node.js e no javascript em geral esse conceito não se difere muito de outras linguagens, sempre que uma função é executada ela entra na stack, que executa somente uma coisa por vez, ou seja, o código posterior ao que está rodando precisa esperar a função atual terminar de executar para seguir adiante. Vamos ver um exemplo:
function generateBornDateFromAge(age) { return 2016 - age; } function generateUserDescription(name, surName, age) { const fullName = name + " " + surName; const bornDate = generateBornDateFromAge(age); return fullName + " is " + age + " old and was born in " + bornDate; } generateUserDescription("Waldemar", "Neto", 26);
Para quem já é familiarizado com javascript não ha nada especial acontecendo aqui. Basicamente, a função generateUserDescription é chamada recebendo nome, sobrenome e idade de um usuário e retorna uma sentença com as informações colhidas. A função generateUserDescription depende da função generateBornDateFromAge para calcular o ano que o usuário nasceu, essa dependência será perfeita para entendermos como a stack funciona.
No momento que a função generateUserInformation é invocada ela vai depender da função generateBornDateFromAge para descobrir o ano em que o usuário nasceu com base no parâmetro age (idade). Quando a função generateBornDateFromAge for invocada pela função generateUserInformation ela será adicionada a stack como no exemplo a seguir:
Conforme a função generateUserInformation vai sendo interpretada, os valores vão sendo atribuídos às respectivas variáveis dentro de seu escopo, como no exemplo do fullName. Para atribuir o valor a variável bornDate foi necessário invocar a função generateBornDateFromAge que quando invocada ela é imediatamente adicionada a stack até que a execução termine e a resposta seja retornada. Após o retorno a stack ficará assim:
O último passo da função será concatenar as variáveis e criar uma frase, isso não irá adicionar mais nada a stack. Quando a função generateUserInformation terminar as demais linhas serão interpretadas, no nosso exemplo será o console.log imprimindo a variável userInformation.
Como a stack só executa uma tarefa por vez foi necessário esperar que a função anterior executasse e finalizasse, para que o console.log pudesse ser adicionado a stack.
Entendendo o funcionamento da stack podemos concluir que funções que precisam de muito tempo para execução irão ocupar mais tempo na stack e assim impedir a chamada das próximas linhas.
Multi threading
Mas o Node.js não é single thread? Essa é a pergunta que os desenvolvedores Node.js provavelmente mais escutam. Na verdade quem é single thread é o V8, motor do google utilizado para rodar o Node.js. A stack que vimos no capitulo anterior faz parte do V8, ou seja, ela é single thread.
Para que seja possível executar tarefas assíncronas o Node.js conta com diversas outras APIs, algumas delas providas pelos próprios sistemas operacionais, como é o caso de eventos de disco, sockets TCP e UDP. Quem toma conta dessa parte de I/O assíncrono, de administrar múltiplas threads e enviar notificações é a libuv.
A libuv é uma biblioteca open source multiplataforma escrita em C, criada inicialmente para o Node.js e hoje usada por diversos outros projetos como Julia e Luvit.
O exemplo a seguir mostra uma função assíncrona sendo executada:
Nesse exemplo a função readFile do módulo de file system do Node.js é executada na stack e jogada para uma thread, a stack segue executando as próximas funções enquanto a função readFile está sendo administrada pela libuv em outra thread. Quando ela terminar o callback sera adicionado a uma fila chamada Task Queue para ser executado pela stack assim que ela estiver livre.
Task Queue
Como vimos no capítulo anterior, algumas ações como I/O são enviadas para serem executadas em outra thread permitindo que o V8 siga trabalhando e a stack siga executando as próximas funções. Essas funções enviadas para que sejam executadas em outra thread precisam de um callback. Um callback é basicamente uma função que será executada quando a função principal terminar.
Esses callbacks podem ter responsabilidades diversas, como por exemplo, chamar outras funções e executar alguma lógica.
Como o V8 é single thread e só existe uma stack, os callbacks precisam esperar a sua vez de serem chamados. Enquanto esperam, os callbacks ficam em um lugar chamado task queue ou fila de tarefas. Sempre que a thread principal finalizar uma tarefa, o que significa que a stack estará vazia, uma nova tarefa é movida da task queue para a stack onde será executada.
Para entender melhor vamos ver a imagem abaixo:
Esse loop, conhecido como Event Loop, é infinito e será responsável por chamar as próximas tarefas da task queue enquanto o Node.js estiver rodando.
Micro e Macro Tasks
Até aqui vimos como funciona a stack, o multithread e também como são enfileirados os callbacks na task queue. Agora vamos conhecer os tipos de tasks (tarefas) que são enfileiradas na task queue, que podem ser micro tasks ou macro tasks.
Macro tasks
Alguns exemplos conhecidos de macro tasks são: setTimeout, I/O, setInterval. Segundo a especificação do WHATWG somente uma macro task deve ser processada em um ciclo do Event Loop.
Micro tasks
Alguns exemplos conhecidos de micro tasks são as promises e o process.nextTick. As micro tasks normalmente são tarefas que devem ser executadas rapidamente após alguma ação, ou realizar algo assíncrono sem a necessidade de inserir uma nova task na task queue.
A especificação do WHATWG diz que após o Event Loop processar a macro task da task queue todas as micro tasks disponíveis devem ser processadas e, caso elas chamem outras micro tasks, essas também devem ser resolvidas para que somente então ele chame a próxima macro task.
O exemplo abaixo demonstra como funciona esse fluxo:
Espero que tenham conseguido entender como o Node.js funciona e também que essa visão ajude vocês a escrevem código de uma maneira que tire mais proveito dessa arquitetura. Aconselho também a lerem os links das referencias, serão muito úteis para o melhor entendimento.
Referencias
[*] https://nodesource.com/blog/understanding-the-nodejs-event-loop/
[*] https://strongloop.com/strongblog/node-js-event-loop/
[*] https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/
[*] https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
[*] https://www.youtube.com/watch?v=8aGhZQkoFbQ