Visión general de bloqueo vs. no bloqueo
Esta descripción general cubre la diferencia entre las llamadas de bloqueo y no bloqueo en Node.js. Esta descripción general se referirá al bucle de eventos y a libuv, pero no se requieren conocimientos previos sobre estos temas. Se supone que los lectores tienen una comprensión básica del lenguaje JavaScript y del patrón de devolución de llamada de Node.js patrón de devolución de llamada.
INFO
"E/S" se refiere principalmente a la interacción con el disco y la red del sistema, compatible con libuv.
Bloqueo
Bloqueo es cuando la ejecución de JavaScript adicional en el proceso Node.js debe esperar hasta que se complete una operación que no sea de JavaScript. Esto sucede porque el bucle de eventos no puede continuar ejecutando JavaScript mientras se está produciendo una operación de bloqueo.
En Node.js, JavaScript que muestra un rendimiento deficiente debido a que consume muchos recursos de la CPU en lugar de esperar una operación que no sea de JavaScript, como E/S, normalmente no se considera bloqueo. Los métodos sincrónicos en la biblioteca estándar de Node.js que usan libuv son las operaciones de bloqueo más comúnmente utilizadas. Los módulos nativos también pueden tener métodos de bloqueo.
Todos los métodos de E/S en la biblioteca estándar de Node.js proporcionan versiones asíncronas, que son no bloqueantes, y aceptan funciones de devolución de llamada. Algunos métodos también tienen contrapartes de bloqueo, cuyos nombres terminan con Sync
.
Comparación de código
Los métodos de bloqueo se ejecutan de forma sincrónica y los métodos de no bloqueo se ejecutan de forma asíncrona.
Usando el módulo del sistema de archivos como ejemplo, esta es una lectura de archivo sincrónica:
const fs = require('node:fs')
const data = fs.readFileSync('/file.md') // bloquea aquí hasta que se lee el archivo
Y aquí hay un ejemplo asíncrono equivalente:
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
if (err) throw err
})
El primer ejemplo parece más simple que el segundo, pero tiene la desventaja de que la segunda línea bloquea la ejecución de cualquier JavaScript adicional hasta que se lee todo el archivo. Tenga en cuenta que en la versión sincrónica, si se produce un error, deberá capturarse o el proceso se bloqueará. En la versión asíncrona, corresponde al autor decidir si un error debe producirse como se muestra.
Ampliemos un poco nuestro ejemplo:
const fs = require('node:fs')
const data = fs.readFileSync('/file.md') // bloquea aquí hasta que se lee el archivo
console.log(data)
moreWork() // se ejecutará después de console.log
Y aquí hay un ejemplo asíncrono similar, pero no equivalente:
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
if (err) throw err
console.log(data)
})
moreWork() // se ejecutará antes de console.log
En el primer ejemplo anterior, console.log
se llamará antes que moreWork()
. En el segundo ejemplo, fs.readFile()
no es bloqueante, por lo que la ejecución de JavaScript puede continuar y moreWork()
se llamará primero. La capacidad de ejecutar moreWork()
sin esperar a que se complete la lectura del archivo es una decisión de diseño clave que permite un mayor rendimiento.
Concurrencia y Rendimiento
La ejecución de JavaScript en Node.js es monohilo, por lo que la concurrencia se refiere a la capacidad del bucle de eventos para ejecutar funciones de devolución de llamada de JavaScript después de completar otro trabajo. Cualquier código que se espera que se ejecute de forma concurrente debe permitir que el bucle de eventos continúe ejecutándose mientras se realizan operaciones que no son de JavaScript, como E/S.
Como ejemplo, consideremos un caso en el que cada solicitud a un servidor web tarda 50 ms en completarse y 45 ms de esos 50 ms son E/S de base de datos que se pueden realizar de forma asincrónica. La elección de operaciones asincrónicas no bloqueantes libera esos 45 ms por solicitud para manejar otras solicitudes. Esta es una diferencia significativa en capacidad simplemente por elegir usar métodos no bloqueantes en lugar de métodos bloqueantes.
El bucle de eventos es diferente a los modelos en muchos otros lenguajes donde se pueden crear subprocesos adicionales para manejar el trabajo concurrente.
Peligros de Mezclar Código Bloqueante y No Bloqueante
Hay algunos patrones que deben evitarse al trabajar con E/S. Veamos un ejemplo:
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
if (err) throw err
console.log(data)
})
fs.unlinkSync('/file.md')
En el ejemplo anterior, es probable que fs.unlinkSync()
se ejecute antes que fs.readFile()
, lo que eliminaría file.md
antes de que se lea realmente. Una mejor manera de escribir esto, que es completamente no bloqueante y garantiza la ejecución en el orden correcto, es:
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
})
})
Lo anterior coloca una llamada no bloqueante a fs.unlink()
dentro de la devolución de llamada de fs.readFile()
, lo que garantiza el orden correcto de las operaciones.