Cómo funciona el modelo de E/S sin bloqueo de un solo hilo en Node.js
(7)
No soy un programador de Node, pero estoy interesado en cómo funciona el modelo IO sin bloqueo de subprocesos únicos . 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(''<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>'');
c.end();
}
);
Aquí viene una pregunta. Cuando hay dos solicitudes, A (aparece primero) y B, ya que solo hay un solo hilo, el programa del lado del servidor manejará la solicitud A en primer lugar: realizar consultas de SQL, que es una declaración de suspensión que espera la E / S en espera. Y el programa está bloqueado en la E / S en espera, y no puede ejecutar el código que hace que la página web quede atrás. ¿Cambiará el programa a solicitar B durante la espera? En mi opinión, debido al modelo de un solo hilo, no hay manera de cambiar una solicitud de otra. Pero el título del código de ejemplo dice que "todo se ejecuta en paralelo, excepto el código". (PD: no estoy seguro si entiendo mal el código o no, ya que nunca he usado Nodo). ¿Cómo cambia Nodo A a B durante la espera? ¿Y puede explicar el modelo de nodo de E / S sin bloqueo de subprocesamiento único de una manera sencilla? Te agradecería si pudieras ayudarme. :)
Bueno, la mayoría de las cosas deberían estar claras hasta ahora ... la parte difícil es el SQL : si en realidad no se está ejecutando en otro subproceso o proceso en su totalidad, la ejecución del SQL debe dividirse en pasos individuales (mediante una ¡Procesador SQL creado para ejecución asíncrona!), Donde se ejecutan los no bloqueadores, y los bloqueadores (por ejemplo, el modo de espera) pueden transferirse al kernel (como una alarma de alarma / evento) y colocarse en la lista de eventos para el bucle principal.
Eso significa que, por ejemplo, la interpretación del SQL, etc. se realiza de inmediato, pero durante la espera (almacenada como un evento que vendrá en el futuro por el núcleo en alguna estructura kqueue, epoll, ...; junto con las otras operaciones de IO ) el bucle principal puede hacer otras cosas y, eventualmente, verificar si sucedió algo de esas IO y esperas.
Entonces, para reformularlo de nuevo: el programa nunca se atasca, las llamadas en espera nunca se ejecutan. Su tarea es realizada por el kernel (escribir algo, esperar que algo pase por la red, esperar que pase el tiempo) u otro hilo o proceso. - El proceso del nodo comprueba si al menos una de esas tareas ha sido completada por el núcleo en la única llamada de bloqueo al sistema operativo una vez en cada ciclo de bucle de eventos. Se llega a ese punto, cuando se hace todo lo que no es de bloqueo.
¿Claro? :-)
No conozco a Node. ¿Pero de dónde viene la pregunta?
Bueno, para dar algo de perspectiva, permítame comparar node.js con apache.
Apache es un servidor HTTP de múltiples subprocesos, para todas y cada una de las solicitudes que recibe el servidor, crea un hilo separado que maneja esa solicitud.
Node.js, por otro lado, está controlado por eventos, manejando todas las solicitudes de forma asíncrona 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 espera los resultados de la consulta antes de servir la página. La página solo se sirve hasta que la consulta haya finalizado. La búsqueda de consultas está bloqueando porque el servidor no puede ejecutar el resto del subproceso hasta que recibe el resultado.
En el nodo, c.query se maneja de forma asíncrona, lo que significa que mientras c.query recupera los resultados para A, salta para manejar c.query para B, y cuando los resultados llegan para A llega, devuelve los resultados 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 un solo hilo, no hay manera de cambiar de una solicitud a otra.
En realidad, el servidor de nodo hace exactamente eso por ti todo el tiempo. Para hacer cambios, (el comportamiento asíncrono) 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 devolución de llamada así como el emisor de eventos para poner en cola las solicitudes de SQL. No los ejecuta de forma asíncrona, eso lo hacen los subprocesos libuv internos que proporcionan la abstracción de E / S no bloqueantes. Los siguientes pasos suceden para hacer una consulta:
- Abra una conexión a db, la conexión en sí se puede hacer de forma asíncrona.
- Una vez que db está conectado, la consulta se pasa al servidor. Las consultas se pueden poner en cola.
- El bucle del evento principal recibe una notificación de la finalización con devolución de llamada o evento.
- El bucle principal ejecuta su callback / eventhandler.
Las solicitudes entrantes al servidor http se manejan de manera similar. La arquitectura interna del hilo es algo como esto:
Los subprocesos de C ++ son los libuv que hacen la E / S asíncrona (disco o red). El bucle de eventos principal continúa ejecutándose después del envío de 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 suceden de esta manera.
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 consulta de base de datos, ahora puede ser manejada por Node.js generando un subproceso de trabajo y asignándole esta tarea de realizar la consulta de base de datos. (Recuerda que Node.js puede crear hilo internamente). Esto permite que la función regrese instantáneamente sin demora.
El segundo argumento "Post-Processing of Data" es una función de devolución de llamada, el marco del nodo registra esta devolución de llamada y es llamado por el bucle de eventos.
Por lo tanto, la declaración c.query (paramenter1, parameter2)
se devolverá instantáneamente, permitiendo al nodo atender a otra solicitud.
PD: Acabo de empezar a entender nodo, en realidad quería escribir esto como un comentario para @Philip pero como no tenía suficientes puntos de reputación, lo escribí como una respuesta.
Node.js se basa en libuv , una biblioteca multiplataforma que extrae apis / syscalls para la entrada / salida asíncrona (sin bloqueo) proporcionada por los sistemas operativos compatibles (Unix, OS X y Windows al menos).
IO asíncrono
En este modelo de programación, la operación de abrir / leer / escribir en dispositivos y recursos (sockets, sistemas de archivos, etc.) administrados por el sistema de archivos no bloquea el subproceso de llamada (como en el modelo c-like síncrono típico) y simplemente marca el proceso (en la estructura de datos a nivel de kernel / OS) para ser notificado cuando haya nuevos datos o eventos disponibles. En el caso de una aplicación similar a un servidor web, el proceso es el responsable de averiguar a qué solicitud / contexto pertenece el evento notificado y proceder a procesar la solicitud desde allí. Tenga en cuenta que esto necesariamente significará que estará en un marco de pila diferente del que originó la solicitud al sistema operativo, ya que este último tuvo que ceder al distribuidor del proceso para que un proceso de un solo hilo pueda manejar nuevos eventos.
El problema con el modelo que describí es que no es familiar y difícil de razonar para el programador, ya que no es de naturaleza secuencial. "Es necesario 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 evento)
Node resuelve el problema aprovechando las características del lenguaje de javascript para hacer que este modelo tenga un aspecto más sincrónico al inducir al programador a emplear un cierto estilo de programación. Cada función que solicita IO tiene una function (... parameters ..., callback)
similar a la firma function (... parameters ..., callback)
y debe 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) para que el sistema operativo indique el tiempo de finalización que se puede dedicar a otros trabajos). La compatibilidad de Javascript con los cierres le permite usar las variables que ha definido en la función externa (llamada) dentro del cuerpo de la devolución de llamada; esto permite mantener el estado entre diferentes funciones que el tiempo de ejecución del nodo invocará de forma independiente. Ver también Estilo de aprobación de aprobació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 bucle invocará la próxima devolución de llamada o función programada para su ejecución (probablemente porque el OS correspondiente notificó el evento correspondiente), lo que permite el procesamiento simultáneo de varias solicitudes.
Puede pensar que el bucle de eventos del nodo es algo similar al distribuidor del kernel: el núcleo programaría la ejecución de un subproceso bloqueado una vez que se complete su E / S 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 hilo al mismo tiempo mediante la multiplexación y la secuenciación de todos sus js la lógica en un solo flujo de ejecución (aunque decir "todo funciona en paralelo" probablemente no sea correcta aquí - vea Concurrencia vs Paralelismo - ¿Cuál es la diferencia? ). Esto funciona bastante bien para los servidores de aplicaciones web, ya que la mayor parte del tiempo se dedica realmente a esperar en la red o en el disco (base de datos / sockets) y la lógica no requiere mucha CPU, es decir: esto funciona bien para cargas de trabajo vinculadas a IO .
Node.js se basa en el modelo de programación de bucle de eventos. El bucle de eventos se ejecuta en un solo hilo y espera repetidamente los eventos y luego ejecuta cualquier controlador de eventos suscrito a esos eventos. Los eventos pueden ser por ejemplo
- la espera del temporizador se ha completado
- La siguiente porción de datos está lista para ser escrita en este archivo.
- Hay una nueva solicitud HTTP que viene de camino.
Todo esto se ejecuta en un solo hilo y ningún código JavaScript se ejecuta en paralelo. Mientras estos manejadores de eventos sean pequeños y esperen aún más eventos, todo funcionará bien. Esto permite que la solicitud múltiple sea manejada simultáneamente por un solo proceso Node.js.
(Hay un poco de magia bajo el capó como el 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, hay muchas cosas (eventos) que suceden 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 mantiene el bombeo de la vida en la aplicación y el avance de otras solicitudes un pequeño evento a la vez. Por lo tanto, se están atendiendo múltiples solicitudes al mismo tiempo.
De acuerdo con: "Bucle de eventos de 10,000 pies - concepto central detrás de Node.js" .
Node.js usa libuv detrás de las escenas. libuv tiene un grupo de subprocesos (de tamaño 4 por defecto). Por lo tanto, Node.js usa hilos 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 hilo de bucle o evento-bucle). Cuando la gente dice "Node.js se ejecuta en un solo hilo" realmente están diciendo "las devoluciones de llamada de Node.js se ejecutan en un solo hilo".
si lee un poco más: "Por supuesto, en el backend, hay subprocesos y procesos para el acceso a la base de datos y la ejecución del proceso. Sin embargo, estos no están expuestos explícitamente a su código, por lo que no puede preocuparse por ellos más que por saber que las interacciones de E / S, por ejemplo, con la base de datos o con otros procesos serán asíncronas desde la perspectiva de cada solicitud, ya que los resultados de esos subprocesos se devuelven a su código a través del bucle de eventos ".
about - "todo se ejecuta en paralelo, excepto su código": su código se ejecuta de forma síncrona, siempre que invoque una operación asíncrona como esperar a IO, el bucle de eventos se encarga de todo e invoca la devolución de llamada. Simplemente no es algo en lo que tengas que pensar.
en su ejemplo: hay dos solicitudes A (primero) y B. usted ejecuta la solicitud A, su código continúa ejecutándose sincrónicamente y ejecuta la solicitud B. el ciclo de eventos maneja la solicitud A, cuando termina, invoca la devolución de llamada de la solicitud A con El resultado, lo mismo va a solicitar B.