¿Por qué un bucle while bloquea el bucle de eventos?
El siguiente ejemplo se proporciona en un libro de Node.js:
var open = false;
setTimeout(function() {
open = true
}, 1000)
while (!open) {
console.log('wait');
}
console.log('open sesame');
Al explicar por qué el bucle while bloquea la ejecución, el autor dice:
Node nunca ejecutará la devolución de llamada de tiempo de espera porque el bucle de eventos está bloqueado en este bucle while iniciado en la línea 7, ¡y nunca le dará la oportunidad de procesar el evento de tiempo de espera!
Sin embargo, el autor no explica por qué sucede esto en el contexto del bucle de eventos o qué sucede realmente bajo el capó.
¿Alguien puede dar más detalles sobre esto? ¿Por qué se atasca el nodo? ¿Y cómo se cambiaría el código anterior, conservando al mismo tiempo la while
estructura de control para que el bucle de eventos no se bloquee y el código se comporte como cabría esperar razonablemente? La espera se registrará durante solo 1 segundo antes de que se setTimeout
dispare y el proceso salga después de registrar "ábrete Sésamo".
Las explicaciones genéricas, como las respuestas a esta pregunta sobre IO, bucles de eventos y devoluciones de llamadas, realmente no me ayudan a racionalizar esto. Espero que una respuesta que haga referencia directa al código anterior ayude.
En realidad, es bastante simple. Internamente, node.js consta de este tipo de bucle:
- Obtener algo de la cola de eventos
- Ejecute cualquier tarea que se le indique y ejecútela hasta que regrese
- Cuando finalice la tarea anterior, obtenga el siguiente elemento de la cola de eventos
- Ejecute cualquier tarea que se le indique y ejecútela hasta que regrese
- Enjuague, haga espuma, repita, una y otra vez.
Si en algún momento no hay nada en la cola de eventos, vaya a dormir hasta que se coloque algo en la cola de eventos o hasta que llegue el momento de activar un temporizador.
Entonces, si una parte de Javascript está en un while()
bucle, entonces esa tarea no está finalizando y, según la secuencia anterior, no se seleccionará nada nuevo de la cola de eventos hasta que la tarea anterior esté completamente completa. Entonces, un bucle muy largo o interminable while()
simplemente arruina el trabajo. Debido a que Javascript solo ejecuta una tarea a la vez (un solo subproceso para la ejecución de JS), si esa tarea gira en un bucle while, entonces nada más podrá ejecutarse.
Aquí hay un ejemplo simple que podría ayudar a explicarlo:
var done = false;
// set a timer for 1 second from now to set done to true
setTimeout(function() {
done = true;
}, 1000);
// spin wait for the done value to change
while (!done) { /* do nothing */}
console.log("finally, the done value changed!");
Algunos podrían pensar lógicamente que el ciclo while girará hasta que se active el temporizador y luego el temporizador cambiará el valor de done
a true
y luego el ciclo while finalizará y se console.log()
ejecutará al final. Eso NO es lo que sucederá. En realidad, esto será un bucle infinito y la console.log()
declaración nunca se ejecutará.
El problema es que una vez que entras en el ciclo de espera while()
, NINGÚN otro Javascript puede ejecutarse. Entonces, el temporizador que quiere cambiar el valor de la done
variable no puede ejecutarse. Por lo tanto, la condición del bucle while nunca puede cambiar y, por tanto, es un bucle infinito.
Esto es lo que sucede internamente dentro del motor JS:
done
variable inicializada afalse
setTimeout()
programa un evento de temporizador durante 1 segundo a partir de ahora- El bucle while comienza a girar
- 1 segundo después de que el ciclo while gire, el temporizador está listo para activarse, pero no podrá hacer nada hasta que el intérprete regrese al ciclo de eventos.
- El bucle while sigue girando porque la
done
variable nunca cambia. Debido a que continúa girando, el motor JS nunca finaliza este hilo de ejecución y nunca extrae el siguiente elemento de la cola de eventos ni ejecuta el temporizador pendiente.
node.js es un entorno impulsado por eventos. Para resolver este problema en una aplicación del mundo real, la done
bandera se cambiaría en algún evento futuro. Entonces, en lugar de un while
ciclo giratorio, registraría un controlador de eventos para algún evento relevante en el futuro y haría su trabajo allí. En el peor de los casos, puede configurar un temporizador recurrente y una "encuesta" para verificar la bandera cada cierto tiempo, pero en casi todos los casos, puede registrar un controlador de eventos para el evento real que hará que la done
bandera cambie y haga tu trabajo en eso. Un código diseñado correctamente que sabe que otro código quiere saber cuándo algo ha cambiado puede incluso ofrecer su propio detector de eventos y sus propios eventos de notificación en los que uno puede registrar interés o incluso simplemente una simple devolución de llamada.
¡Esta es una gran pregunta pero encontré una solución!
var sleep = require('system-sleep')
var done = false
setTimeout(function() {
done = true
}, 1000)
while (!done) {
sleep(100)
console.log('sleeping')
}
console.log('finally, the done value changed!')
Creo que funciona porque system-sleep
no es una espera de giro.
Hay otra solución. Puede obtener acceso al bucle de eventos en casi todos los ciclos.
let done = false;
setTimeout(() => {
done = true
}, 5);
const eventLoopQueue = () => {
return new Promise(resolve =>
setImmediate(() => {
console.log('event loop');
resolve();
})
);
}
const run = async () => {
while (!done) {
console.log('loop');
await eventLoopQueue();
}
}
run().then(() => console.log('Done'));
El nodo es una única tarea en serie. No hay paralelismo y su concurrencia está limitada por IO. Piénselo así: todo se ejecuta en un solo subproceso, cuando realiza una llamada IO que está bloqueando/sincrónica, su proceso se detiene hasta que se devuelven los datos; sin embargo, digamos que tenemos un solo subproceso que, en lugar de esperar en IO (leer el disco, tomar una URL, etc.), su tarea continúa con la siguiente tarea y, una vez completada, verifica esa IO. Esto es básicamente lo que hace el nodo, es un "bucle de eventos" que sondea IO para completar (o progresar) en un bucle. Entonces, cuando una tarea no se completa (su bucle), el bucle de eventos no progresa. Para hacerlo mas simple.