Cómo funciona el modelo de E / S sin locking de un solo hilo en Node.js

No soy un progtwigdor de nodos, pero estoy interesado en cómo funciona el modelo de IO sin locking de un solo hilo . Después de leer el artículo understanding-the-node-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('Hello

Return from async DB query

'); c.end(); } );

Aquí viene una pregunta. Cuando hay dos solicitudes A (viene primero) y B, dado que solo hay un hilo único, el progtwig del lado del servidor manejará la solicitud A en primer lugar: haciendo consultas SQL, que es una statement de reposo para las espera de E / S. Y el progtwig está trabado en la espera de E / S, y no puede ejecutar el código que hace que la página web se quede atrás. ¿Cambiará el progtwig para solicitar 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 tu código”. (PD. No estoy seguro si malinterpreto el código o no, ya que nunca he usado Node). ¿Cómo cambia Node A a B durante la espera? ¿Y puede explicar el modelo de IO no bloqueante de un solo hilo de una manera simple? Apreciaría si pudieras ayudarme. 🙂

Node.js se basa en libuv , una biblioteca multiplataforma que abstrae apis / syscalls para entradas / salidas asincrónicas (sin locking) proporcionadas por los sistemas operativos compatibles (al menos, Unix, OS X y Windows).

IO asíncrono

En este modelo de progtwigción, la operación de apertura / lectura / escritura en dispositivos y recursos (sockets, sistema de archivos, etc.) administrada 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 del nivel 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 averiguar 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 stack diferente al que originó la solicitud al sistema operativo ya que este último tuvo que ceder el paso al despachador de un proceso para que un solo proceso de subprocesamiento maneje nuevos eventos.

El problema con el modelo que describí es que no es familiar y difícil de razonar para el progtwigdor, ya que es de naturaleza no secuencial. “Necesita hacer una solicitud en la función A y manejar el resultado en una función diferente donde sus locales de A generalmente no están disponibles”.

Modelo de nodo (Continuing Passing Style y Event Loop)

Node aborda el problema aprovechando las características del lenguaje de JavaScript para hacer que este modelo tenga un aspecto un poco más sincrónico al inducir al progtwigdor a emplear un cierto estilo de progtwigción. Cada función que solicita IO tiene una function (... parameters ..., callback) tipo firma function (... parameters ..., callback) y necesita recibir una callback que se invocará cuando se complete la operación solicitada (tenga en cuenta que la mayor parte del tiempo se usa esperando para que el sistema operativo indique la finalización, tiempo que se puede dedicar a otro trabajo). El soporte de Javascript para cierres le permite usar variables que ha definido en la función externa (llamada) dentro del cuerpo de la callback; esto permite mantener el estado entre diferentes funciones que serán invocadas por el tiempo de ejecución del nodo de forma independiente. Consulte también Estilo de paso de continuación .

Además, después de invocar una función que genera una operación IO, la función de llamada generalmente return control al bucle de eventos del nodo. Este ciclo invocará la siguiente callback o función que se programó para la ejecución (muy probablemente porque el OS ha notificado el evento correspondiente), lo que permite el procesamiento simultáneo de múltiples solicitudes.

Puedes pensar en el ciclo de eventos del nodo como algo similar al despachador del kernel: el kernel progtwigría la ejecución de un hilo bloqueado una vez que se complete su IO pendiente, mientras que el nodo progtwigrá una callback cuando se haya producido 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 que permite que su código maneje solicitudes de cientos de miles de socket abierto con un único hilo al mismo tiempo multiplexando y secuenciando todos sus js lógica en un único flujo de ejecución (aunque decir “todo se ejecuta en paralelo” probablemente no sea correcto aquí – ver Concurrencia vs Paralelismo – ¿Cuál es la diferencia? ). Esto funciona bastante bien para los servidores webapp ya que la mayor parte del tiempo se usa para esperar la red o el disco (bases de datos / sockets) y la lógica no requiere mucha CPU, es decir, funciona bien para cargas de trabajo vinculadas a IO .

Bueno, para dar una perspectiva, permítame comparar node.js con apache.

Apache es un servidor HTTP multiproceso, para cada solicitud que recibe el servidor, crea un hilo separado que maneja esa solicitud.

Node.js, por otro lado, es impulsado por eventos, manejando todas las solicitudes de forma asincrónica desde un solo hilo.

Cuando A y B se reciben en apache, se crean dos subprocesos que manejan las solicitudes. Cada uno maneja la consulta por separado, cada uno esperando los resultados de la consulta antes de servir a la página. La página solo se sirve hasta que finaliza la consulta. La búsqueda de consulta está bloqueando porque el servidor no puede ejecutar el rest del hilo hasta que reciba el resultado.

En el nodo, c.query se maneja de forma asíncrona, lo que significa que mientras c.query obtiene los resultados para A, salta para manejar c.query para B, y cuando los resultados llegan para A llega a enviar los resultados a la callback que envía el respuesta. Node.js sabe ejecutar la callback cuando finaliza la búsqueda.

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 nodos 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 el estilo de callback y el emisor de eventos para poner en cola las solicitudes de SQL. No los ejecuta asincrónicamente, eso es hecho por los hilos libuv internos que proporcionan la abstracción de E / S sin locking. Los siguientes pasos ocurren para hacer una consulta:

  1. Abra una conexión a db, la conexión en sí misma puede realizarse de forma asíncrona.
  2. Una vez que db está conectado, la consulta se pasa al servidor. Las consultas se pueden poner en cola.
  3. El bucle de evento principal se notifica de la finalización con callback o evento.
  4. El bucle principal ejecuta su callback / manejador de eventos.

Las solicitudes entrantes al servidor http se manejan de la misma manera. La architecture interna de subprocesos es algo como esto:

loop de evento node.js

Los hilos C ++ son los libuv que hacen la E / S asincrónica (disco o red). El bucle de evento 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 usa subprocesos para lograr concurrencia.

Sin embargo , su código se ejecuta en un solo subproceso (es decir, todas las devoluciones de llamada de las funciones de Node.js se invocarán en el mismo subproceso, lo que se denomina bucle-thread o event-loop). Cuando la gente dice “Node.js se ejecuta en un único hilo”, realmente están diciendo “las devoluciones de llamada de Node.js se ejecutan en un solo hilo”.

Node.js se basa en el modelo de progtwigción de bucle de eventos. El bucle de eventos se ejecuta en un solo subproceso y espera repetidamente eventos y luego ejecuta los controladores de eventos suscritos a esos eventos. Los eventos pueden ser, por ejemplo,

  • la espera del temporizador está completa
  • el próximo fragmento de datos está listo para escribirse en este archivo
  • hay una nueva solicitud HTTP nueva que viene en nuestro camino

Todo esto se ejecuta en un solo hilo y nunca se ejecuta código JavaScript en paralelo. Mientras estos controladores de eventos sean pequeños y esperen aún más eventos, todo funciona bien. Esto permite que múltiples solicitudes sean manejadas simultáneamente por un solo proceso Node.js.

(Hay un poco de magia bajo el capó como en donde se originan los eventos. Algunos de ellos implican subprocesos de trabajo de bajo nivel que se ejecutan en paralelo).

En este caso de SQL, hay muchas cosas (eventos) que ocurren entre hacer la consulta de la base de datos y obtener sus resultados en la callback . Durante ese tiempo, el ciclo de eventos continúa dando vida a la aplicación y avanzando otras solicitudes un pequeño evento a la vez. Por lo tanto, se están atendiendo múltiples solicitudes al mismo tiempo.

bucle de eventos vista de alto nivel

De acuerdo con: “Bucle de evento desde 10,000ft – concepto central detrás de Node.js” .

La función c.query () tiene dos argumentos

 c.query("Fetch Data", "Post-Processing of Data") 

La operación “Fetch Data” en este caso es una DB-Query, ahora esto puede ser manejado por Node.js generando un hilo de trabajo y dándole esta tarea de realizar el DB-Query. (Recuerde que Node.js puede crear subprocesos internamente). Esto permite que la función regrese instantáneamente sin demora

El segundo argumento “Postprocesamiento de datos” es una función de callback, la estructura del nodo registra esta callback y es invocada por el bucle de evento.

Por lo tanto, la statement c.query (paramenter1, parameter2) volverá instantáneamente, permitiendo que el nodo atienda otra solicitud.

PD: Acabo de empezar a entender el nodo, en realidad quería escribir esto como comentario para @Philip, pero como no tenía suficientes puntos de reputación, lo escribí como respuesta.

si lees un poco más: “Por supuesto, en el back-end, hay hilos y procesos para acceso a bases de datos y ejecución de procesos. Sin embargo, estos no están explícitamente expuestos a tu código, por lo que no puedes preocuparte por ellos más que conociendo que las interacciones de E / S, por ejemplo, con la base de datos o con otros procesos serán asincrónicas desde la perspectiva de cada solicitud, ya que los resultados de esos hilos se devuelven a través del bucle de evento a su código “.

sobre – “todo se ejecuta en paralelo excepto su código” – su código se ejecuta de forma síncrona, cada vez que invoca una operación asíncrona, como esperar IO, el bucle de evento maneja todo e invoca la callback. simplemente no es algo en lo que tienes que pensar.

en su ejemplo: hay dos solicitudes A (viene primero) y B. usted ejecuta la solicitud A, su código continúa ejecutándose sincrónicamente y ejecuta la solicitud B. el bucle de evento maneja la solicitud A, cuando termina invoca la callback de la solicitud A con el resultado, lo mismo pasa con la solicitud B.

De acuerdo, la mayoría de las cosas deben quedar claras hasta ahora … la parte difícil es el SQL : si no se está ejecutando en realidad en otro hilo o proceso en su totalidad, la ejecución de SQL se debe dividir en pasos individuales (por un ¡Procesador SQL hecho para ejecución asíncrona!), Donde se ejecutan los no bloqueantes, y los bloqueadores (por ejemplo, el reposo) pueden transferirse al kernel (como una alarma interrupción / evento) y poner en la lista de eventos para el bucle principal.

Eso significa, por ejemplo, la interpretación del SQL, etc. se realiza de inmediato, pero durante la espera (almacenado como un evento que vendrá en el futuro por el kernel en alguna estructura kqueue, epoll, … junto con las otras operaciones IO ) el ciclo principal puede hacer otras cosas y eventualmente verificar si sucedió algo de esos IO y espera.

Por lo tanto, para reformularlo de nuevo: el progtwig nunca está (permitido) atascado, las llamadas a dormir nunca se ejecutan. Su deber es hecho por el kernel (escriba algo, espere que algo venga a través de la red, esperando que pase el tiempo) u otro hilo o proceso. – El proceso Nodo comprueba si al menos uno de esos deberes es completado por el kernel en la única llamada de locking al sistema operativo una vez en cada evento-ciclo-ciclo. Ese punto se alcanza, cuando todo lo que no se bloquea se hace.

¿Claro? 🙂

No sé Nodo. Pero, ¿de dónde viene la búsqueda?