Pular para o conteúdo

JavaScript Intermediário #3
Event Loop

Entenda, com uma visão ampla, o modelo de concorrência da linguagem JavaScript

Nesse texto, vamos compreender um pouco mais sobre um paradigma importante quando estamos programando em JavaScript e, além disso, entender uma das principais características que levaram ao sucesso da linguagem: o seu modelo de concorrência.

Assincronia em JavaScript 🙝

Programação assíncrona é um paradigma de programação onde o código pode não seguir o fluxo de execução “padrão”. Além disso, um código assíncrono pode tratar de interrupções externas ao programa, tais como a chegada de um evento, sinal, temporizador, entre outras.

Em JavaScript, podemos escrever código assíncrono de forma fácil utilizando a função setTimeout, que recebe dois parâmetros: uma função a ser executada e uma quantidade de milissegundos que indica o tempo de espera para iniciar a execução dessa função. Veja o exemplo abaixo:

function foo() {
  setTimeout(function () {
    console.log('without one.');
  }, 1000);

  setTimeout(function () {
    console.log('fully dressed');
  }, 0);

  console.log('You know');
}

console.log('Smile, my dear!');
foo();
console.log('you are not');

A saída do código acima será escrita como:

Smile, my dear!
You know
you are not
fully dressed
without one.

Apesar dos logs serem colocados fora de ordem, alguns foram colocados em funções que foram passadas para o setTimeout e que serão executadas somente após o tempo de espera for esgotado. Vamos ver outro exemplo um pouco mais complexo. Considere o código abaixo:

function asyncCountTo(x) {
  for (var i = 0; i < x; i++) {
    setTimeout(
      function () {
        console.log(i);
      },
      (x - i) * 1000,
    );
  }
}

asyncCountTo(4);

Qual é a saída do código acima? Temos um laço que dispara várias funções por meio do setTimeout e que imprimem a variável i. Por maior que seja a surpresa, o código acima exibe como saída:

4
4
4
4

Por quê? A razão desse comportamento se deve à closure associada a função passada para o setTimeout. A variável i é compartilhada por todas as closures e, por isso, tem esse comportamento. O laço continua sendo executado e a variável i continua sendo incrementada até chegue ao valor quatro, que é quando a condição do laço falha. Só então os callbacks do setTimeout são executados.

No entanto, você percebeu que mesmo quando o tempo de espera for zero, a função não é executada imediatamente? Bem, aqui estamos de frente com um dos efeitos do modelo de concorrência do JavaScript e que vamos entrar em detalhes a partir de agora.

Programação Orientada a Eventos 🙝

O JavaScript é uma linguagem que foi inicialmente desenvolvida para adicionar funcionalidades as páginas HTML do navegador Netscape. É uma linguagem multiparadigma que suporta programação procedural, orientada a objetos e funcional, mas, foi especialmente desenvolvida para atender a programação orientada a eventos.

A programação orientada a eventos ajusta o fluxo do programa de acordo com eventos, ou seja, interrupções que podem ser feitas a qualquer momento e necessitam de “reações” por parte do programa. É um paradigma muito utilizado para o desenvolvimento de drivers e sensores de microcontroladores. A programação orientada a eventos também predomina no desenvolvimento de aplicações com GUI, pois requerem que a aplicação reaja de acordo com a interação do usuário. Não é por coincidência que ela seria utilizada em “uma linguagem que quer adicionar algumas funcionalidades em páginas HTML”, certo?

Em uma aplicação orientada a eventos, normalmente temos um Laço de Eventos — comumente chamado de event loop — que aguarda os eventos acontecerem para chamar funções que foram designadas à responder um evento específico. E é o comportamento desse laço o qual vamos detalhar daqui em diante.

O Event Loop do JavaScript 🙝

Toda máquina de execução JavaScript possui um event loop único que captura os eventos disparados pelo usuário ou pelo ambiente onde a máquina está alocada. Se um evento capturado pelo event loop possuir algum callback associado — uma função que deve ser executada quando o evento ocorrer — então uma tarefa será enfileirada na fila de tarefas que irá iniciar a execução do callback pelo motor de execução JavaScript.

A fila de tarefas, também chamada de job queue, é uma estrutura de fila que armazena referência para funções que devem ser executadas. De forma geral, o motor de execução do JavaScript possui um algoritmo bastante simples:

  1. Aguarde a fila ter tarefas;
  2. Execute a primeira tarefa da fila até o fim, isto é, até que a pilha de execução esteja vazia.
  3. Retorne ao passo 1.

Na verdade, a função setTimeout que vimos anteriormente não “executa uma função após uma quantidade de tempo”, mas, adiciona no event loop a função como um callback para um sinal de um temporizador que será disparado após a quantidade de tempo do segunto parâmetro. Existem outras formas de adicionar tarefas a fila, entre elas:

O event loop é um modelo de concorrência não preemptivo. Não é possível interromper a execução da função corrente e retornar posteriormente. Somente uma função é executada por vez. Uma vez que a computação de uma função é iniciada, ela não é mais interrompida. Assim, não há paralelismo de execução das funções no event loop.

É por isso que, mesmo quando colocado no setTimeout com tempo de espera zero, o callback não é executado imediatamente. Ainda é necessário terminar a execução da função atual e, somente então, o callback é executado. Isso explica o comportamendo da saída do código do exemplo anterior. Na realidade, o setTimeout não garante que a função vai ser executada após o período, mas sim que sua tarefa será enfileirada.

Outro efeito colateral desse modelo de concorrência é que, se alguma tarefa demanda muita computação, ela inevitavelmente vai bloquear a fila de tarefas por muito tempo e, assim, nenhuma outra tarefa poderá ser iniciada. Se a fila de tarefas está bloqueada, os callbacks dos eventos das ações do usuário podem não ser respondidos em tempo adequado e a GUI apresenta estar “travada”, por exemplo.

Indo além 🙝

Agora que você já entende o modelo de concorrência, alguns questionamentos podem estar perambulando pela sua cabeça:

Mas, quando fazemos uma requisição HTTP utilizando a API fetch, o usuário ainda é capaz de interagir com a tela mesmo que a requisição demore vários segundos para ser concluída… Como isso acontece se o event loop está bloqueado?

Diferentes máquinas virtuais JavaScript implementam algumas funcionalidades em fluxos de execução paralelos ao event loop para não bloquear a fila de tarefas por muito tempo. Normalmente tais funcionalidades estão relacionadas a operações de entrada e saída que, comumente, são operações “lentas”. Alguns exemplos de operações de entrada e saída que normalmente são executadas paralelamente ao event loop incluem: leitura e escrita de arquivos, requisições de rede, stream de multimídia, acesso ao banco de dados, entre outros.

Essa publicação apresentou uma versão bastante simplificada de como o event loop do JavaScript funciona. Ainda há vários pontos que foram deixados de lado para o bem da sanidade da explicação. No entanto, acredito que você tenha conseguido entender o funcionamento das máquinas virtuais JavaScript e compreendido o modelo de concorrência da linguagem e as consequências que surgem a partir do seu uso. Caso você tenha interesse em uma visão mais completa, recomendo ler mais sobre:

Max Naegeler Roecker

Mestre em Ciência da Computação & Desenvolvedor de Software