Front End

9 dez, 2016

Node.js: o que é esse Event Loop, afinal?

Publicidade

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 está aqui e é mais introdutória, falando sobre 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
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 têm 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 há 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.

stack1

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:

stack2

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, é imediatamente adicionada à stack até que a execução termine e a resposta seja retornada. Após o retorno, a stack ficará assim:

stack3

O último passo da função será concatenar as variáveis e criar uma frase. Isso não irá adicionar mais nada à 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.

stack4

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 à 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, o motor do google utilizado para rodar o Node.js. A stack que vimos no capítulo 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. Hoje ela é usada por diversos outros projetos, como Julia e Luvit.

O exemplo a seguir mostra uma função assíncrona sendo executada:

stack5

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, será adicionado à uma fila chamada Task Queue para ser executado pela stack assim que ela estiver livre.

stack6

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, eles 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:

stack7

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 e 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:

stack8

Espero que tenham conseguido entender como o Node.js funciona e também que essa visão ajude vocês a escreverem códigos de uma maneira que tire mais proveito dessa arquitetura. Aconselho também a lerem os links das referencias, que serão muito úteis para o melhor entendimento.

Referencias