Skip to content

Visão geral de Bloqueio vs. Não Bloqueio

Esta visão geral abrange a diferença entre chamadas de bloqueio e não bloqueio no Node.js. Esta visão geral fará referência ao loop de eventos e ao libuv, mas nenhum conhecimento prévio desses tópicos é necessário. Assume-se que os leitores tenham um conhecimento básico da linguagem JavaScript e do padrão de retorno de chamada do Node.js [/guide/javascript-asynchronous-programming-and-callbacks].

INFO

"E/S" refere-se principalmente à interação com o disco e a rede do sistema suportados pelo libuv.

Bloqueio

Bloqueio ocorre quando a execução de JavaScript adicional no processo Node.js deve aguardar até que uma operação não JavaScript seja concluída. Isso acontece porque o loop de eventos não consegue continuar executando JavaScript enquanto uma operação de bloqueio estiver ocorrendo.

No Node.js, JavaScript que apresenta desempenho ruim devido a ser intensivo em CPU em vez de aguardar uma operação não JavaScript, como E/S, normalmente não é referido como bloqueio. Os métodos síncronos na biblioteca padrão do Node.js que usam libuv são as operações de bloqueio mais comumente usadas. Módulos nativos também podem ter métodos de bloqueio.

Todos os métodos de E/S na biblioteca padrão do Node.js fornecem versões assíncronas, que são não bloqueantes, e aceitam funções de retorno de chamada. Alguns métodos também têm contrapartes de bloqueio, que têm nomes que terminam com Sync.

Comparando Código

Os métodos de bloqueio são executados sincronamente e os métodos não bloqueantes são executados assincronamente.

Usando o módulo do Sistema de Arquivos como exemplo, esta é uma leitura de arquivo síncrona:

js
const fs = require('node:fs')
const data = fs.readFileSync('/file.md') // bloqueia aqui até que o arquivo seja lido

E aqui está um exemplo assíncrono equivalente:

js
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
  if (err) throw err
})

O primeiro exemplo parece mais simples que o segundo, mas tem a desvantagem de a segunda linha bloquear a execução de qualquer JavaScript adicional até que o arquivo inteiro seja lido. Observe que na versão síncrona, se um erro for lançado, ele precisará ser capturado ou o processo será interrompido. Na versão assíncrona, cabe ao autor decidir se um erro deve ser lançado, como mostrado.

Vamos expandir um pouco nosso exemplo:

js
const fs = require('node:fs')
const data = fs.readFileSync('/file.md') // bloqueia aqui até que o arquivo seja lido
console.log(data)
moreWork() // será executado após o console.log

E aqui está um exemplo assíncrono semelhante, mas não equivalente:

js
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
  if (err) throw err
  console.log(data)
})
moreWork() // será executado antes do console.log

No primeiro exemplo acima, console.log será chamado antes de moreWork(). No segundo exemplo, fs.readFile() é não bloqueante, portanto, a execução do JavaScript pode continuar e moreWork() será chamado primeiro. A capacidade de executar moreWork() sem esperar que a leitura do arquivo seja concluída é uma escolha de design fundamental que permite maior taxa de transferência.

Concorrência e Throughput

A execução JavaScript no Node.js é monofilamentada, então concorrência se refere à capacidade do loop de eventos de executar funções de callback JavaScript após completar outros trabalhos. Qualquer código que se espera que seja executado de forma concorrente deve permitir que o loop de eventos continue rodando enquanto operações não-JavaScript, como I/O, estão ocorrendo.

Como exemplo, vamos considerar um caso onde cada requisição a um servidor web leva 50ms para completar e 45ms desses 50ms são I/O de banco de dados que podem ser feitos assincronamente. Escolher operações assíncronas não-bloqueantes libera esses 45ms por requisição para lidar com outras requisições. Esta é uma diferença significativa na capacidade apenas por escolher usar métodos não-bloqueantes em vez de métodos bloqueantes.

O loop de eventos é diferente de modelos em muitas outras linguagens onde threads adicionais podem ser criadas para lidar com trabalho concorrente.

Perigos de Misturar Código Bloqueante e Não-Bloqueante

Existem alguns padrões que devem ser evitados ao lidar com I/O. Vamos ver um exemplo:

js
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
  if (err) throw err
  console.log(data)
})
fs.unlinkSync('/file.md')

No exemplo acima, fs.unlinkSync() provavelmente será executado antes de fs.readFile(), o que excluiria file.md antes que ele seja realmente lido. Uma maneira melhor de escrever isso, que é completamente não-bloqueante e garante a execução na ordem correta é:

js
const fs = require('node:fs')
fs.readFile('/file.md', (readFileErr, data) => {
  if (readFileErr) throw readFileErr
  console.log(data)
  fs.unlink('/file.md', unlinkErr => {
    if (unlinkErr) throw unlinkErr
  })
})

O código acima coloca uma chamada não-bloqueante para fs.unlink() dentro do callback de fs.readFile(), o que garante a ordem correta das operações.