javascript - style - title tag in html
¿Cómo procesa el nodo las solicitudes concurrentes? (6)
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:
Últimamente he estado leyendo nodejs, intentando entender cómo maneja múltiples solicitudes concurrentes, sé que nodejs es una arquitectura basada en un solo bucle de eventos de subprocesos, en un momento dado solo se ejecutará una declaración, es decir, en el hilo principal y el código de bloqueo / 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 pila 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 estas solicitudes?
¿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 devolución de llamada), 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.
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.
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 diagrama 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 su código, su 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 entran.
En resumen, el NodeJS
que NodeJS
sea ''single threaded'' es verdadero, sin embargo, es solo un detector 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.
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 pasar 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'' }
para que se imprima mediante el proceso secundario bifurcado y luego el proceso secundario bifurcado enviará un valor de contador incrementado cada segundo para ser impreso por El proceso padre.
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 mal nombre en este contexto, ya que funciona más como una cola o pila)
al final del archivo js, V8 verificará si hay mensajes o mensajes en ejecución en el bucle de eventos. Si no hay ninguno, saldrá de 0 (por eso 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 no se puede producir ninguna otra invocación de bucle de eventos durante su ejecución; Solo cuando está terminado, se puede invocar otra función de bucle de evento.
Parte 2, la devolución de llamada interna
La misma lógica se aplica también a la devolución de llamada interna, y puede usarse para explicar por qué el programa 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 la callback
de callback
y 1 llamada a la inner
.
callback
es la siguiente función en línea, por lo que se invocará a continuación. El proceso se repite. Se añade 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 ocurre en C ++ precompilado, 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 que, en lugar de esperar una cantidad de tiempo establecida, agrega la devolución de llamada al bucle de eventos en las solicitudes http entrantes.
Por lo tanto, asumamos 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 asumir que son uno tras otro, con un tiempo insignificante entre ellos.
Cualquiera que sea la solicitud que llegue primero, será manejada primero por el hilo 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 devolución de llamada 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 devolución de llamada de use
, sucederá lo siguiente:
- Se imprimirá el primer registro de la consola.
- readImage iniciará un subproceso de trabajador y regresará inmediatamente.
- 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 devolución de llamada y el uso de un subproceso de trabajo es lo que lo hace asíncrono.
Después de que este use
devuelve la devolución de llamada, 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
devolución de llamada 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 devolución de llamada al bucle de eventos. Por lo tanto, se ejecutarán a continuación, en orden de la persona que primero recupere sus datos. 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 devolución de llamada readImage comenzará a ejecutarse: el registro de la consola y la escritura en la respuesta.
en este punto, ambos subprocesos 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 otra cosa 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ó.