Cómo funciona el modelo IO sin bloqueo de un solo subproceso en Node.js
No soy programador de Node, pero estoy interesado en cómo funciona el modelo IO sin bloqueo de un solo subproceso . Después de leer el artículo sobre cómo entender-el-nodo-js-event-loop , estoy realmente confundido al respecto. Dio un ejemplo para el modelo:
c.query(
'SELECT SLEEP(20);',
function (err, results, fields) {
if (err) {
throw err;
}
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
c.end();
}
);
Que: Cuando hay dos solicitudes A (viene primero) y B, ya que solo hay un subproceso, el programa del lado del servidor manejará la solicitud A en primer lugar: realizar consultas SQL es una declaración inactiva que representa la espera de E/S. Y el programa está atascado en I/O
espera y no puede ejecutar el código que muestra la página web detrás. ¿El programa cambiará a la solicitud B durante la espera? En mi opinión, debido al modelo de subproceso único, no hay forma de cambiar una solicitud de otra. Pero el título del código de ejemplo dice que todo se ejecuta en paralelo excepto su código .
(PD: No estoy seguro si entendí mal el código o no, ya que nunca he usado Node). ¿Cómo cambia Node de A a B durante la espera? ¿Y puede explicar el modelo IO sin bloqueo de un solo subproceso de Node de una manera sencilla? Te agradecería si pudieras ayudarme. :)
Node.js se basa en libuv , una biblioteca multiplataforma que abstrae apis/syscalls para entradas/salidas asincrónicas (sin bloqueo) proporcionadas por los sistemas operativos compatibles (Unix, OS X y Windows al menos).
E/S asincrónicas
En este modelo de programación, la operación de apertura/lectura/escritura en dispositivos y recursos (sockets, sistema de archivos, etc.) administrados por el sistema de archivos no bloquea el hilo de llamada (como en el típico modelo síncrono tipo C) y simplemente marca el proceso (en la estructura de datos a nivel de kernel/OS) para recibir notificaciones cuando haya nuevos datos o eventos disponibles. En el caso de una aplicación similar a un servidor web, el proceso es responsable de determinar a qué solicitud/contexto pertenece el evento notificado y continuar procesando la solicitud desde allí. Tenga en cuenta que esto necesariamente significará que estará en un marco de pila diferente al que originó la solicitud al sistema operativo, ya que este último tuvo que ceder ante el despachador de un proceso para que un proceso de un solo subproceso maneje nuevos eventos.
El problema con el modelo que describí es que no es familiar y difícil de razonar para el programador, ya que es de naturaleza no secuencial. "Debe realizar una solicitud en la función A y manejar el resultado en una función diferente donde los locales de A generalmente no están disponibles".
Modelo de nodo (estilo de paso de continuación y bucle de eventos)
Node aborda el problema aprovechando las características del lenguaje javascript para hacer que este modelo tenga un aspecto un poco más sincrónico al inducir al programador a emplear un determinado estilo de programación. Cada función que solicita IO tiene una firma similar function (... parameters ..., callback)
y necesita recibir una devolución de llamada que se invocará cuando se complete la operación solicitada (tenga en cuenta que la mayor parte del tiempo se pasa esperando que el sistema operativo indique la finalización; tiempo que puede ser dedicado a otros trabajos). El soporte de Javascript para cierres le permite usar variables que haya definido en la función externa (de llamada) dentro del cuerpo de la devolución de llamada; esto permite mantener el estado entre diferentes funciones que serán invocadas por el tiempo de ejecución del nodo de forma independiente. Véase también Estilo de pase de continuación .
Además, después de invocar una función que genera una operación IO, la función que llama generalmente controlará el bucle de eventosreturn
del nodo . Este bucle invocará la siguiente devolución de llamada o función que estaba programada para su ejecución (muy probablemente porque el sistema operativo notificó el evento correspondiente); esto permite el procesamiento simultáneo de múltiples solicitudes.
Puede pensar en el bucle de eventos del nodo como algo similar al despachador del kernel : el kernel programaría la ejecución de un hilo bloqueado una vez que se complete su IO pendiente, mientras que el nodo programará una devolución de llamada cuando haya ocurrido el evento correspondiente.
Altamente concurrente, sin paralelismo
Como observación final, la frase "todo se ejecuta en paralelo excepto su código" hace un trabajo decente al capturar el punto en el que el nodo permite que su código maneje solicitudes de cientos de miles de sockets abiertos con un solo subproceso simultáneamente al multiplexar y secuenciar todos sus js. lógica en un único flujo de ejecución (aunque decir "todo se ejecuta en paralelo" probablemente no sea correcto aquí; consulte Concurrencia versus paralelismo: ¿cuál es la diferencia? ). Esto funciona bastante bien para servidores de aplicaciones web, ya que la mayor parte del tiempo se dedica a esperar la red o el disco (base de datos/sockets) y la lógica no requiere realmente un uso intensivo de la CPU, es decir: esto funciona bien para cargas de trabajo vinculadas a IO .
Bueno, para dar una perspectiva, permítanme comparar node.js con apache.
Apache es un servidor HTTP de subprocesos múltiples; para todas y cada una de las solicitudes que recibe el servidor, crea un subproceso separado que maneja esa solicitud.
Node.js, por otro lado, está controlado por eventos y maneja todas las solicitudes de forma asincrónica desde un solo subproceso.
Cuando A y B se reciben en Apache, se crean dos subprocesos que manejan las solicitudes. Cada uno maneja la consulta por separado y espera los resultados de la consulta antes de publicar la página. La página solo se sirve hasta que finaliza la consulta. La recuperación de la consulta se está bloqueando porque el servidor no puede ejecutar el resto del hilo hasta que reciba el resultado.
En el nodo, c.query se maneja de forma asincrónica, lo que significa que mientras c.query obtiene los resultados para A, salta para manejar c.query para B, y cuando llegan los resultados para A, los devuelve a la devolución de llamada, que envía el respuesta. Node.js sabe ejecutar la devolución de llamada cuando finaliza la recuperación.
En mi opinión, debido a que es un modelo de subproceso único, no hay forma de cambiar de una solicitud a otra.
En realidad, el servidor de nodo hace exactamente eso por usted todo el tiempo. Para realizar cambios (el comportamiento asincrónico), la mayoría de las funciones que usaría tendrán devoluciones de llamada.
Editar
La consulta SQL se toma de la biblioteca mysql . Implementa un estilo de devolución de llamada y un emisor de eventos para poner en cola las solicitudes SQL. No los ejecuta de forma asincrónica, eso lo hacen los subprocesos internos de libuv que proporcionan la abstracción de E/S sin bloqueo. Para realizar una consulta se siguen los siguientes pasos:
- Abra una conexión a la base de datos; la conexión en sí se puede realizar de forma asincrónica.
- Una vez que la base de datos está conectada, la consulta se pasa al servidor. Las consultas se pueden poner en cola.
- El bucle de eventos principal recibe una notificación de finalización con una devolución de llamada o un evento.
- El bucle principal ejecuta su devolución de llamada/eventhandler.
Las solicitudes entrantes al servidor http se manejan de manera similar. La arquitectura del hilo interno es algo como esto:
Los subprocesos de C++ son los libuv que realizan la E/S asíncrona (disco o red). El bucle de eventos principal continúa ejecutándose después de enviar la solicitud al grupo de subprocesos. Puede aceptar más solicitudes ya que no espera ni duerme. Las consultas SQL/solicitudes HTTP/lecturas del sistema de archivos ocurren de esta manera.
Node.js usa libuv detrás de escena. libuv tiene un grupo de subprocesos (de tamaño 4 por defecto). Por lo tanto, Node.js utiliza subprocesos para lograr la concurrencia.
Sin embargo , su código se ejecuta en un solo hilo (es decir, todas las devoluciones de llamada de las funciones de Node.js se llamarán en el mismo hilo, el llamado bucle-hilo o bucle de eventos). Cuando la gente dice "Node.js se ejecuta en un solo subproceso", en realidad están diciendo "las devoluciones de llamada de Node.js se ejecutan en un solo subproceso".
Node.js se basa en el modelo de programación de bucle de eventos. El bucle de eventos se ejecuta en un solo subproceso y espera repetidamente eventos y luego ejecuta cualquier controlador de eventos suscrito a esos eventos. Los eventos pueden ser por ejemplo
- la espera del temporizador está completa
- El siguiente fragmento de datos está listo para escribirse en este archivo.
- Hay una nueva solicitud HTTP en camino
Todo esto se ejecuta en un solo subproceso y nunca se ejecuta ningún código JavaScript en paralelo. Mientras estos controladores de eventos sean pequeños y esperen más eventos, todo funcionará bien. Esto permite que un único proceso de Node.js maneje varias solicitudes simultáneamente.
(Hay un poco de magia bajo el capó en cuanto al lugar donde se originan los eventos. Algunos de ellos involucran subprocesos de trabajo de bajo nivel que se ejecutan en paralelo).
En este caso de SQL, suceden muchas cosas (eventos) entre realizar la consulta de la base de datos y obtener sus resultados en la devolución de llamada . Durante ese tiempo, el bucle de eventos sigue dando vida a la aplicación y avanzando otras solicitudes, un pequeño evento a la vez. Por lo tanto, se atienden varias solicitudes al mismo tiempo.
Según: "Bucle de eventos desde 10,000 pies: concepto central detrás de Node.js" .