Fundamentos do Node.js

0/23 aulas0%
teoria

O Event Loop: Como Node.js Lida com Concorrência

Aprenda sobre o event loop: como node.js lida com concorrência

30 min
Aula 1 de 6

O Event Loop: Como Node.js Lida com Concorrência

Olá, futuros ninjas do Node.js! 👋 Nesta aula, vamos desvendar um dos conceitos mais fundamentais e, por vezes, mais mal compreendidos do Node.js: o Event Loop. Entender como ele funciona é crucial para escrever aplicações Node.js eficientes, não bloqueantes e de alta performance.

1. Introdução: O Coração Assíncrono do Node.js ❤️

Você já deve ter ouvido que Node.js é "single-threaded" (monotarefa). Isso é verdade para a execução do seu código JavaScript. No entanto, como ele consegue lidar com milhares de requisições concorrentes sem bloquear o thread principal? A resposta está no Event Loop!

O Event Loop é um mecanismo que permite ao Node.js realizar operações de I/O (Input/Output), como requisições de rede, acesso a banco de dados ou leitura de arquivos, de forma não bloqueante. Isso significa que, enquanto uma operação lenta de I/O está acontecendo em segundo plano, seu código JavaScript pode continuar executando outras tarefas, em vez de ficar esperando.

Imagine um restaurante com um único chef (o thread JavaScript). Se cada pedido demorasse para ser preparado e o chef só pudesse pegar o próximo pedido depois de terminar o atual, o serviço seria muito lento. O Event Loop é como ter vários ajudantes na cozinha (threads de I/O) que preparam os pratos mais demorados, enquanto o chef principal se concentra em pegar novos pedidos e finalizar os pratos que já estão prontos.

2. Explicação Detalhada: Os Componentes e Fases do Event Loop 🔄

Para entender o Event Loop, precisamos conhecer seus principais componentes e as fases que ele percorre.

2.1. Os Componentes Essenciais

  1. Call Stack (Pilha de Chamadas): É onde o seu código JavaScript é executado. Funções são adicionadas à pilha quando são chamadas e removidas quando retornam. O JavaScript é single-threaded porque só há uma Call Stack.
  2. Node.js APIs (libuv): Quando você chama funções assíncronas em Node.js (ex: fs.readFile, http.get, setTimeout), elas são delegadas a essas APIs. Essas APIs, por sua vez, utilizam threads de I/O (gerenciados pela biblioteca libuv) para executar as operações demoradas em segundo plano, sem bloquear a Call Stack.
  3. Callback Queue (ou Task Queue / Macrotask Queue): É uma fila onde os callbacks das operações assíncronas (como setTimeout, setImmediate, eventos de I/O) são colocados após suas operações serem concluídas.
  4. Microtask Queue: Uma fila de prioridade mais alta que a Callback Queue. Contém callbacks de process.nextTick() e Promises (.then(), .catch(), .finally()).

2.2. O Ciclo de Vida do Event Loop (As Fases)

O Event Loop opera em um ciclo contínuo, verificando se há trabalho a ser feito em diferentes fases. A ordem das fases é crucial:

Event Loop Phases Diagram Fonte: Node.js Official Documentation

  1. timers: Esta fase executa callbacks agendados por setTimeout() e setInterval().
  2. pending callbacks: Executa callbacks do sistema operacional que foram adiados na iteração anterior. Por exemplo, alguns erros de I/O.
  3. poll:
    • Verifica novas conexões de I/O e dados.
    • Executa callbacks de I/O quase todos os outros.
    • Se não houver timers agendados que estejam prontos e a check queue estiver vazia, o Event Loop pode bloquear aqui e esperar por novas operações de I/O.
  4. check: Esta fase executa callbacks agendados por setImmediate().
  5. close callbacks: Executa callbacks para eventos de close, como socket.on('close', ...).

Importante: Entre cada fase do Event Loop, o Node.js esvazia a Microtask Queue. Isso significa que todos os process.nextTick() e callbacks de Promises pendentes serão executados antes que o Event Loop passe para a próxima fase ou para a próxima iteração.

2.3. process.nextTick() vs setImmediate() vs setTimeout(fn, 0)

Esta é uma das maiores fontes de confusão. Vamos esclarecer:

  • process.nextTick(callback):

    • Não faz parte do Event Loop diretamente.
    • É executado imediatamente após a conclusão da operação atual na Call Stack, e antes do Event Loop avançar para a próxima fase.
    • Tem a mais alta prioridade entre os agendadores assíncronos que veremos.
    • É esvaziado completamente antes de qualquer outra macrotarefa ou antes de passar para a próxima fase.
  • setTimeout(callback, delay):

    • Agendado para ser executado após um delay mínimo (em milissegundos).
    • Se delay for 0, o callback é colocado na fila de timers na próxima oportunidade.
    • Sua execução depende da fase timers do Event Loop.
  • setImmediate(callback):

    • Agendado para ser executado na fase check do Event Loop.
    • Geralmente executa depois dos setTimeout(fn, 0) (se não houver I/O) e antes de qualquer novo I/O.
    • É ideal para desmembrar operações pesadas em pequenos pedaços, liberando o Event Loop para outras tarefas.

Exemplo de Ordem de Execução (Sem I/O):

console.log('Início do script');
 
setTimeout(() => {
  console.log('setTimeout 0');
}, 0);
 
setImmediate(() => {
  console.log('setImmediate');
});
 
process.nextTick(() => {
  console.log('process.nextTick');
});
 
Promise.resolve().then(() => {
  console.log('Promise.resolve');
});
 
console.log('Fim do script');

Saída esperada (sem I/O):

Início do script
Fim do script
process.nextTick
Promise.resolve
setTimeout 0
setImmediate

Explicação:

  1. Início do script e Fim do script são síncronos, executam primeiro.
  2. process.nextTick é executado imediatamente após o código síncrono e antes do Event Loop iniciar suas fases.
  3. Promise.resolve (microtask) é executado logo após process.nextTick e antes das macrotasks.
  4. O Event Loop entra na fase timers. setTimeout(0) é executado.
  5. O Event Loop entra na fase check. setImmediate é executado.

Exemplo de Ordem de Execução (Com I/O):

Quando há I/O, a ordem entre setTimeout(0) e setImmediate() pode variar, pois setImmediate() é garantido para ser executado na fase check e setTimeout(0) na fase timers. Se uma operação de I/O for concluída e seu callback for adicionado à fila antes dos timers serem processados, setImmediate() pode ser executado antes de setTimeout(0).

import fs from 'fs'; // Importação ES Modules, ou use const fs = require('fs') para CommonJS
 
console.log('Início do script');
 
fs.readFile('nao_existe.txt', (err, data) => {
  if (err) {
    console.log('Erro ao ler arquivo (callback de I/O)');
  }
  setImmediate(() => {
    console.log('setImmediate dentro do callback de I/O');
  });
  setTimeout(() => {
    console.log('setTimeout dentro do callback de I/O');
  }, 0);
});
 
setTimeout(() => {
  console.log('setTimeout 0 (global)');
}, 0);
 
setImmediate(() => {
  console.log('setImmediate (global)');
});
 
process.nextTick(() => {
  console.log('process.nextTick (global)');
});
 
Promise.resolve().then(() => {
  console.log('Promise.resolve (global)');
});
 
console.log('Fim do script');

Saída possível (pode variar ligeiramente dependendo do sistema):

Início do script
Fim do script
process.nextTick (global)
Promise.resolve (global)
Erro ao ler arquivo (callback de I/O)
setImmediate dentro do callback de I/O
setTimeout dentro do callback de I/O
setTimeout 0 (global)
setImmediate (global)

Explicação:

  1. Início e Fim síncronos.
  2. process.nextTick e Promise.resolve (microtasks) globais executam.
  3. A operação fs.readFile é delegada à libuv.
  4. O Event Loop entra na fase timers. setTimeout 0 (global) é agendado, mas ainda não executa.
  5. O Event Loop entra na fase poll. A operação fs.readFile é concluída (com erro) e seu callback é adicionado à fila.
  6. O callback de I/O (Erro ao ler arquivo...) é executado. Dentro dele, setImmediate e setTimeout(0) são agendados.
  7. Após o callback de I/O, o Event Loop verifica a Microtask Queue (vazia).
  8. O Event Loop avança para a fase check. setImmediate dentro do callback de I/O é executado.
  9. O Event Loop avança para a fase close callbacks.
  10. O Event Loop começa uma nova iteração. Na fase timers, setTimeout dentro do callback de I/O e setTimeout 0 (global) são executados.
  11. Na fase check, setImmediate (global) é executado.

A chave é que setImmediate() é executado na fase check, enquanto setTimeout(0) é executado na fase timers. Quando um callback de I/O é executado, ele está na fase poll. Após a fase poll, vem a fase check, o que significa que setImmediate() agendado dentro de um callback de I/O será executado antes de um setTimeout(0) agendado no mesmo contexto.

3. Código de Exemplo Oficial (Adaptado e Atualizado) 📜

A documentação oficial do Node.js oferece um excelente exemplo para demonstrar a diferença entre setImmediate() e setTimeout(). Vamos adaptar para incluir process.nextTick e Promises para uma visão mais completa.

import fs from 'fs'; // Para ES Modules. Use 'const fs = require('fs');' para CommonJS.
 
function compareTimers() {
  console.log('1. Início do script');
 
  // Agendando um nextTick
  process.nextTick(() => {
    console.log('2. process.nextTick callback');
  });
 
  // Agendando uma Promise
  Promise.resolve().then(() => {
    console.log('3. Promise.resolve callback');
  });
 
  // Agendando um setTimeout com delay de 0ms
  setTimeout(() => {
    console.log('4. setTimeout callback (delay 0)');
  }, 0);
 
  // Agendando um setImmediate
  setImmediate(() => {
    console.log('5. setImmediate callback');
  });
 
  // Operação de I/O para demonstrar interação
  // Criamos um arquivo temporário e o lemos.
  fs.writeFile('temp_file.txt', 'Olá, Event Loop!', () => {
    console.log('6. fs.writeFile callback (operação de I/O concluída)');
 
    // Agendando outro nextTick e Promise dentro do callback de I/O
    process.nextTick(() => {
      console.log('7. process.nextTick dentro do I/O callback');
    });
    Promise.resolve().then(() => {
      console.log('8. Promise.resolve dentro do I/O callback');
    });
 
    // Agendando um setTimeout e setImmediate dentro do callback de I/O
    setTimeout(() => {
      console.log('9. setTimeout dentro do I/O callback');
    }, 0);
 
    setImmediate(() => {
      console.log('10. setImmediate dentro do I/O callback');
    });
 
    // Limpeza do arquivo temporário
    fs.unlink('temp_file.txt', (err) => {
      if (err) console.error('Erro ao remover arquivo temporário:', err);
      console.log('11. temp_file.txt removido');
    });
  });
 
  console.log('12. Fim do script (código síncrono)');
}
 
compareTimers();

Possível Saída (pode haver pequenas variações dependendo da performance do sistema para I/O):

1. Início do script
12. Fim do script (código síncrono)
2. process.nextTick callback
3. Promise.resolve callback
6. fs.writeFile callback (operação de I/O concluída)
7. process.nextTick dentro do I/O callback
8. Promise.resolve dentro do I/O callback
10. setImmediate dentro do I/O callback
4. setTimeout callback (delay 0)
5. setImmediate callback
9. setTimeout dentro do I/O callback
11. temp_file.txt removido

Análise da Saída:

  1. Os logs síncronos (1. Início e 12. Fim) são executados primeiro.
  2. process.nextTick e Promise.resolve (globais) são executados em seguida, pois são microtasks e têm prioridade sobre as fases do Event Loop.
  3. A operação fs.writeFile é concluída (ela é assíncrona e usa threads de I/O). Seu callback (6. fs.writeFile callback) é executado.
  4. Dentro do callback de I/O, novos process.nextTick e Promise.resolve são agendados. Como estamos ainda no contexto de um callback de I/O (que é uma macrotask), o Event Loop esvazia a Microtask Queue antes de avançar para a próxima fase. Por isso, 7. process.nextTick dentro do I/O callback e 8. Promise.resolve dentro do I/O callback são executados logo após o callback de I/O.
  5. O Event Loop avança para a fase check. 10. setImmediate dentro do I/O callback é executado (pois ele foi agendado dentro de um callback de I/O, que está na fase poll, e a fase check vem logo após poll).
  6. O Event Loop começa uma nova iteração. Na fase timers, 4. setTimeout callback (delay 0) é executado.
  7. O Event Loop avança para a fase check. 5. setImmediate callback é executado.
  8. O Event Loop começa outra iteração. Na fase timers, 9. setTimeout dentro do I/O callback é executado.
  9. A operação fs.unlink é concluída e seu callback (11. temp_file.txt removido) é executado.

Este exemplo complexo ilustra a prioridade das microtasks e como a interação com operações de I/O pode afetar a ordem de setTimeout(0) e setImmediate().

4. Resumo e Próximos Passos 🚀

Pontos Chave:

  • Node.js é single-threaded para o seu código JavaScript, mas usa o Event Loop e threads de I/O (via libuv) para lidar com operações assíncronas de forma não bloqueante.
  • O Event Loop possui fases (timers, pending callbacks, poll, check, close callbacks) que ele percorre em um ciclo contínuo.
  • process.nextTick() e Promises (.then()) são microtasks e têm a mais alta prioridade, executando entre as fases do Event Loop e antes de qualquer macrotask (callbacks de setTimeout, setImmediate, I/O).
  • setImmediate() é executado na fase check.
  • setTimeout(fn, 0) é executado na fase timers. A ordem entre setImmediate e setTimeout(0) pode variar dependendo se há operações de I/O ativas.

Compreender o Event Loop é fundamental para otimizar suas aplicações Node.js, evitar bloqueios e depurar comportamentos assíncronos inesperados.

Próximos Passos:

  • Experimente! Modifique os exemplos de código, adicione mais console.log em diferentes pontos e observe a saída.
  • Aprofunde-se em libuv: Se você se sentir aventureiro, pesquise sobre a biblioteca libuv, que é a base do Event Loop e das operações de I/O do Node.js.
  • Assista a palestras: Há excelentes palestras sobre o Event Loop (como as de Philip Roberts ou Jake Archibald) que visualizam o processo de forma interativa.
  • Módulos Core: Na próxima aula, exploraremos módulos core do Node.js que utilizam intensivamente esses conceitos assíncronos, como fs para manipulação de arquivos e http para servidores web.

Parabéns por desvendar este conceito crucial! Continue praticando e explorando! 🌟

© 2025 Escola All Dev. Todos os direitos reservados.

O Event Loop: Como Node.js Lida com Concorrência - Fundamentos do Node.js | escola.all.dev.br