¿Cómo procesa el nodo las solicitudes concurrentes?

He estado leyendo nodejs últimamente, tratando de entender cómo maneja múltiples solicitudes concurrentes, sé que nodejs es una architecture basada en un solo bucle de eventos de subprocesos, en un momento dado solo se ejecutará una statement, es decir, en el hilo principal y el código de locking Las llamadas IO son manejadas por los hilos de trabajo (el valor predeterminado es 4).

Ahora mi pregunta es qué sucede cuando un servidor web creado con nodejs recibe múltiples solicitudes, lo sé, hay muchos hilos de desbordamiento de stack que tienen preguntas similares, pero no encontraron una respuesta concreta a esto.

Así que estoy poniendo un ejemplo aquí, digamos que tenemos el siguiente código dentro de una ruta como / index .

app.use('/index', function(req, res, next) { console.log("hello index routes was invoked"); readImage("path", function(err, content) { status = "Success"; if(err) { console.log("err :", err); status = "Error" } else { console.log("Image read"); } return res.send({ status: status }); }); var a = 4, b = 5; console.log("sum =", a + b); }); 

Supongamos que readImage () tarda alrededor de 1 minuto en leer esa imagen. Si dos solicitudes T1 y T2 llegaron de manera concurrente, ¿cómo los nodos procesarán esta solicitud?

¿Tomará la primera solicitud T1, la procesará mientras está en cola la solicitud T2 ( corríjame si mi entendimiento está equivocado aquí) , si se encuentra algo asíncrono / bloqueado como readImage, luego se lo envía al subproceso de trabajo (algún punto después) cuando se hace async stuff, notifica al hilo principal y el hilo principal comienza a ejecutar la callback), avanza ejecutando la siguiente línea de código. Cuando se hace con T1, entonces recoge la solicitud de T2? ¿Es correcto? o puede procesar el código T2 en el medio (es decir, mientras se llama readImage, puede comenzar a procesar T2)?

Realmente apreciaría si alguien me puede ayudar a encontrar una respuesta a esta pregunta.

Su confusión podría provenir de no enfocarse lo suficiente en el bucle de eventos. claramente tienes una idea de cómo funciona esto, pero tal vez no sea la imagen completa.

Parte 1, fundamentos de bucle de eventos

Cuando llama al método de use , lo que sucede detrás de escena es otro hilo creado para escuchar las conexiones.

Sin embargo, cuando llega una solicitud, ya que estamos en un subproceso diferente al motor V8 (y no podemos invocar directamente la función de enrutamiento), se agrega una llamada serializada a la función en el bucle de eventos compartido , para que se llame más tarde. . (el bucle de eventos es un nombre pobre en este contexto, ya que funciona más como una cola o stack)

al final del archivo js, ​​V8 verificará si hay algún thead o mensaje en ejecución en el bucle de eventos. Si no hay ninguno, saldrá de 0 (esta es la razón por la cual el código del servidor mantiene el proceso en ejecución). Por lo tanto, lo primero que hay que entender es que no se procesará ninguna solicitud hasta que se llegue al final sincrónico del archivo js.

Si se agregó el bucle de eventos mientras se estaba iniciando el proceso, cada llamada de función en el bucle de eventos se manejará una por una, en su totalidad, de forma síncrona.

Para simplificar, déjame dividir tu ejemplo en algo más expresivo.

 function callback() { setTimeout(function inner() { console.log('hello inner!'); }, 0); // † console.log('hello callback!'); } setTimeout(callback, 0); setTimeout(callback, 0); 

setTimeout con un tiempo de 0, es una forma rápida y fácil de poner algo en el bucle de eventos sin complicaciones del temporizador, ya que no importa qué, siempre ha sido de al menos 0 ms.

En este ejemplo, la salida siempre será:

 hello callback! hello callback! hello inner! hello inner! 

Ambas llamadas serializadas a callback de callback se agregan al bucle de eventos antes de que se llame a cualquiera de ellas, garantizado . Esto sucede porque no se puede invocar nada desde el bucle de eventos hasta después de la ejecución síncrona completa del archivo.

Puede ser útil pensar en la ejecución de su archivo, como lo primero en el bucle de eventos. Debido a que cada invocación desde el bucle de eventos solo puede suceder en serie, se convierte en una consecuencia lógica, que ninguna otra invocación de bucle de eventos puede ocurrir durante su ejecución; Solo cuando está terminado, se puede invocar otra función de bucle de evento.

Parte 2, la callback interna

La misma lógica se aplica también a la callback interna, y puede usarse para explicar por qué el progtwig nunca generará:

 hello callback! hello inner! hello callback! hello inner! 

Como puedes esperar.

Al final de la ejecución del archivo, habrá 2 llamadas de función serializadas en el bucle de eventos, ambas para callback de callback . Como el bucle de eventos es un FIFO (primero en setTimeout , primero en salir), el setTimeout que vino primero, se invocará primero.

Lo primero que hace la callback es realizar otro setTimeout . Como antes, esto agregará una llamada serializada, esta vez a la función inner , al bucle de eventos. setTimeout regresa inmediatamente y la ejecución se moverá al primer console.log .

En este momento, el bucle de eventos se ve así:

 1 [callback] (executing) 2 [callback] (next in line) 3 [inner] (just added by callback) 

El retorno de la callback de callback es la señal para que el bucle de eventos elimine esa invocación de sí mismo. Esto deja 2 cosas en el bucle de eventos ahora: 1 llamada más a callback de callback , y 1 llamada a llamada inner .

callback es la siguiente función en línea, por lo que se invocará a continuación. El proceso se repite. Se agrega una llamada a inner al ciclo de eventos. Un console.log imprime Hello Callback! y terminamos eliminando esta invocación de callback de callback del bucle de eventos.

Esto deja el bucle de eventos con 2 funciones más:

 1 [inner] (next in line) 2 [inner] (added by most recent callback) 

Ninguna de estas funciones se mete con el bucle de eventos, se ejecutan una tras otra; El segundo esperando el regreso del primero. Luego, cuando el segundo vuelve, el bucle de eventos se deja vacío. Esto, combinado con el hecho de que no hay otros subprocesos en ejecución, desencadena el final del proceso. salida 0.

Parte 3, relacionada con el ejemplo original

Lo primero que sucede en su ejemplo, es que se crea un subproceso, dentro del proceso, que creará un servidor vinculado a un puerto en particular. Tenga en cuenta que esto sucede en C ++ precomstackdo, no en javascript, y no es un proceso separado, es un subproceso dentro del mismo proceso. ver: Tutorial de C ++ Thread

Así que ahora, cuando se recibe una solicitud, la ejecución de su código original no se verá afectada. En su lugar, las solicitudes de conexión entrantes se abrirán, se mantendrán y se agregarán al bucle de eventos.

La función de use , es la puerta de enlace para capturar los eventos para las solicitudes entrantes. Es una capa de abstracción, pero en aras de la simplicidad, es útil pensar en la función de use como si fuera un setTimeout . Excepto, en lugar de esperar una cantidad de tiempo establecida, agrega la callback al bucle de eventos en las solicitudes http entrantes.

Por lo tanto, asummos que hay dos solicitudes que llegan al servidor: T1 y T2. En tu pregunta, dices que entran al mismo tiempo, ya que esto es técnicamente imposible, voy a suponer que son uno tras otro, con un tiempo insignificante entre ellos.

Cualquiera que sea la solicitud que llegue primero, será manejada primero por el subproceso secundario de antes. Una vez que se ha abierto esa conexión, se agrega al bucle de eventos, y pasamos a la siguiente solicitud, y repetimos.

En cualquier momento después de agregar la primera solicitud al bucle de eventos, V8 puede comenzar la ejecución de la callback de use .


un lado rápido sobre readImage

Dado que no está claro si readImage es de una biblioteca en particular, algo que usted escribió o no, es imposible decir exactamente qué hará en este caso. Sin embargo, solo hay 2 posibilidades, así que aquí están:

 // in this example definition of readImage, its entirely // synchronous, never using an alternate thread or the // event loop function readImage (path, callback) { let image = fs.readFileSync(path); callback(null, image); // a definition like this will force the callback to // fully return before readImage returns. This means // means readImage will block any subsequent calls. } // in this alternate example definition its entirely // asynchronous, and take advantage of fs' async // callback. function readImage (path, callback) { fs.readFile(path, (err, data) => { callback(err, data); }); // a definition like this will force the readImage // to immediately return, and allow exectution // to continue. } 

A los fines de la explicación, estaré operando bajo el supuesto de que readImage volverá de inmediato, como deberían ser las funciones asíncronas apropiadas.


Una vez que se inicie la ejecución de callback de use , sucederá lo siguiente:

  1. Se imprimirá el primer registro de la consola.
  2. readImage iniciará un subproceso de trabajador y regresará inmediatamente.
  3. El segundo registro de la consola se imprimirá.

Durante todo esto, es importante tener en cuenta que estas operaciones se realizan de forma sincrónica; Ninguna otra invocación de bucle de evento puede comenzar hasta que estas hayan finalizado. readImage puede ser asíncrono, pero al llamarlo no, la callback y el uso de un subproceso de trabajo es lo que lo hace asíncrono.

Después de que este use devuelve la callback, es probable que la siguiente solicitud ya haya terminado de analizar y se haya agregado al bucle de eventos, mientras que V8 estaba ocupado haciendo los registros de la consola y la llamada de lectura de imagen.

Por lo tanto, se invoca el siguiente use callback y se repite el mismo proceso: registro, inicie un subproceso readImage, vuelva a iniciar sesión, regrese.

Después de este punto, las Imágenes leídas (dependiendo de cuánto tiempo tomen) probablemente ya hayan recuperado lo que necesitaban y agregaron su callback al bucle de eventos. Por lo tanto, se ejecutarán a continuación, en el orden en que uno recupere sus datos primero. recuerde, estas operaciones se realizaban en subprocesos separados, por lo que sucedieron no solo paralelas al subproceso principal de javascript, sino también paralelas entre sí, por lo que aquí, no importa a quién se llamó primero, importa cuál terminó primero, y obtuvo dibs en el bucle de eventos.

Cualquiera que sea readImage completado primero será el primero en ejecutarse. por lo tanto, suponiendo que no haya errores , imprimiremos en la consola y luego escribiremos en la respuesta para la solicitud correspondiente, mantenida en el ámbito léxico.

Cuando ese envío vuelva, la próxima callback readImage comenzará a ejecutarse: el registro de la consola y la escritura en la respuesta.

en este punto, ambos subprocesos de readImage han muerto y el bucle de eventos está vacío, pero el subproceso que contiene el enlace del puerto del servidor mantiene el proceso activo, esperando que se agregue algo más al bucle de eventos y el ciclo continúe.

Espero que esto le ayude a comprender la mecánica detrás de la naturaleza asíncrona del ejemplo que proporcionó.

Para cada solicitud entrante, el nodo lo manejará uno por uno. Eso significa que debe haber orden, al igual que la cola, primero en el primer servicio. Cuando el nodo comienza a procesar la solicitud, se ejecutará todo el código síncrono y lo asíncrono pasará al subproceso de trabajo, por lo que el nodo puede comenzar a procesar la siguiente solicitud. Cuando la parte asíncrona esté terminada, volverá al hilo principal y continuará.

Entonces, cuando su código síncrono demora demasiado, bloquea el hilo principal, el nodo no podrá manejar otra solicitud, es fácil de probar.

 app.use('/index', function(req, res, next) { // synchronous part console.log("hello index routes was invoked"); var sum = 0; // useless heavy task to keep running and block the main thread for (var i = 0; i < 100000000000000000; i++) { sum += i; } // asynchronous part, pass to work thread readImage("path", function(err, content) { // when work thread finishes, add this to the end of the event loop and wait to be processed by main thread status = "Success"; if(err) { console.log("err :", err); status = "Error" } else { console.log("Image read"); } return res.send({ status: status }); }); // continue synchronous part at the same time. var a = 4, b = 5; console.log("sum =", a + b); }); 

El nodo no comenzará a procesar la siguiente solicitud hasta que finalice todas las partes síncronas. Así que la gente dice que no bloqueen el hilo principal.

Simplemente puede crear un proceso secundario cambiando la función readImage () en un archivo diferente usando fork ().

El archivo padre, parent.js :

 const { fork } = require('child_process'); const forked = fork('child.js'); forked.on('message', (msg) => { console.log('Message from child', msg); }); forked.send({ hello: 'world' }); 

El archivo hijo, child.js :

 process.on('message', (msg) => { console.log('Message from parent:', msg); }); let counter = 0; setInterval(() => { process.send({ counter: counter++ }); }, 1000); 

Artículo anterior podría ser útil para usted.

En el archivo principal anterior, child.js (que ejecutará el archivo con el comando de nodo) y luego escuchamos el evento del message . El evento de message se emitirá cada vez que el hijo use process.send , lo que haremos cada segundo.

Para transmitir los mensajes del padre al hijo, podemos ejecutar la función de send en el propio objeto bifurcado, y luego, en el script del niño, podemos escuchar el evento del message en el objeto de process global.

Al ejecutar el archivo parent.js anterior, primero enviará el objeto { hello: 'world' } que se imprimirá mediante el proceso secundario bifurcado y luego el proceso secundario bifurcado enviará un valor de contador incrementado cada segundo que se imprimirá El proceso padre.

Hay una serie de artículos que explican esto como este.

Lo largo y corto de todo esto es que nodejs no es realmente una aplicación de un solo hilo, es una ilusión. El diagtwig en la parte superior del enlace anterior lo explica razonablemente bien, sin embargo, como un resumen

  • El bucle de eventos NodeJS se ejecuta en un solo hilo
  • Cuando recibe una solicitud, entrega esa solicitud a un nuevo hilo.

Entonces, en tu código, tu aplicación en ejecución tendrá un PID de 1 por ejemplo. Cuando recibe la solicitud T1, crea un PID 2 que procesa esa solicitud (en 1 minuto). Mientras se ejecuta, obtienes la solicitud T2 que genera un PID 3 que también demora 1 minuto. Tanto el PID 2 como el 3 terminarán después de que se complete su tarea, sin embargo, el PID 1 continuará escuchando y entregando los eventos a medida que ingresan.

En resumen, el NodeJS que NodeJS sea ​​’single threaded’ es verdadero, sin embargo, es solo un detector de bucle de eventos. Cuando se escuchan los eventos (solicitudes), los pasa a un grupo de subprocesos que se ejecutan de forma asíncrona, lo que significa que no bloquea otras solicitudes.

El interpeter V8 JS (es decir: Nodo) es básicamente un solo hilo. Pero, los procesos que inicia pueden ser asíncronos, por ejemplo: ‘fs.readFile’.

A medida que se ejecuta el servidor Express, abrirá nuevos procesos según sea necesario para completar las solicitudes. Por lo tanto, la función ‘readImage’ se iniciará (generalmente de forma asíncrona), lo que significa que volverán en cualquier orden. Sin embargo, el servidor gestionará qué respuesta va a qué solicitud automáticamente.

Por lo tanto, NO tendrá que administrar qué respuesta de readImage va a qué solicitud.

Básicamente, T1 y T2 no regresarán simultáneamente, esto es virtualmente imposible. Ambos dependen en gran medida del sistema de archivos para completar la “lectura” y pueden terminar en CUALQUIER ORDEN (esto no se puede predecir). Tenga en cuenta que los procesos son manejados por la capa del sistema operativo y son por naturaleza multiproceso (en una computadora moderna).

Si está buscando un sistema de colas, no debería ser demasiado difícil de implementar / asegurar que las imágenes se lean / devuelvan en el orden exacto en que se solicitaron.

Dado que realmente no hay más que agregar a la respuesta anterior de Marcus, aquí hay un gráfico que explica el mecanismo de bucle de eventos de un solo hilo:

introduzca la descripción de la imagen aquí