Forma correcta de escribir una función sin bloqueo en Node.js

Resuelto nosbor asked hace 5 años • 0 respuestas

Escribí una función simple que devuelve Promise, por lo que no debería ser bloqueante (en mi opinión). Desafortunadamente, parece que el programa deja de esperar a que finalice la Promesa. No estoy seguro de qué puede estar mal aquí.

function longRunningFunc(val, mod) {
    return new Promise((resolve, reject) => {
        sum = 0;
        for (var i = 0; i < 100000; i++) {
            for (var j = 0; j < val; j++) {
                sum += i + j % mod
            }
        }
        resolve(sum)
    })
}

console.log("before")
longRunningFunc(1000, 3).then((res) => {
    console.log("Result: " + res)
})
console.log("after")

El resultado parece el esperado:

before     // delay before printing below lines
after
Result: 5000049900000

Pero el programa espera antes de imprimir la segunda y tercera línea. ¿Puede explicar cuál debería ser la forma correcta de imprimir primero el "antes" y el "después" y luego (después de un tiempo) el resultado?

nosbor avatar Dec 21 '18 04:12 nosbor
Aceptado

Envolver el código en una promesa (como lo ha hecho usted) no lo convierte en no bloqueante. La función ejecutora de Promise (la devolución de llamada a la que pasa new Promise(fn)se llama sincrónicamente y se bloqueará, por lo que ve el retraso en la obtención del resultado).

De hecho, no hay forma de crear su propio código Javascript simple (como el que tiene) que no bloquee, excepto ponerlo en un proceso secundario, usando un WorkerThread, usando alguna biblioteca de terceros que cree nuevos hilos de Javascript o usando las nuevas API experimentales de node.js para subprocesos. Node.js normal ejecuta su Javascript como bloqueo y de un solo subproceso, ya sea que esté incluido en una promesa o no.

Puede usar cosas como setTimeout()cambiar "cuándo" se ejecuta su código, pero cada vez que se ejecuta, seguirá bloqueándose (una vez que comienza a ejecutarse, no se puede ejecutar nada más hasta que termine). Todas las operaciones asincrónicas en la biblioteca node.js usan alguna forma de código nativo subyacente que les permite ser asincrónicas (o simplemente usan otras API asincrónicas de node.js que a su vez usan implementaciones de código nativo).

Pero el programa espera antes de imprimir la segunda y tercera línea. ¿Puede explicar cuál debería ser la forma correcta de imprimir primero el "antes" y el "después" y luego (después de un tiempo) el resultado?

Como dije anteriormente, envolver cosas en la función ejecutora de promesas no las hace asincrónicas. Si desea "cambiar" el momento en que se ejecutan las cosas (aunque todavía están sincrónicas), puede usar un setTimeout(), pero eso en realidad no hace que nada sea no bloqueante, solo hace que se ejecute más tarde (aún bloqueando cuando se ejecuta).

Entonces, podrías hacer esto:

function longRunningFunc(val, mod) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            sum = 0;
            for (var i = 0; i < 100000; i++) {
                for (var j = 0; j < val; j++) {
                    sum += i + j % mod
                }
            }
            resolve(sum)
        }, 10);
    })
}

Eso reprogramaría el forbucle que consume mucho tiempo para que se ejecute más tarde y podría "parecer" que no bloquea, pero en realidad todavía se bloquea, simplemente se ejecuta más tarde. Para que sea verdaderamente sin bloqueo, tendrías que usar una de las técnicas mencionadas anteriormente para sacarlo del hilo principal de Javascript.

Formas de crear código real sin bloqueo en node.js:

  1. Ejecútelo en un proceso secundario separado y reciba una notificación asincrónica cuando esté terminado.
  2. Utilice los nuevos subprocesos de trabajo experimentales en node.js v11
  3. Escriba su propio complemento de código nativo para node.js y use subprocesos libuv o subprocesos a nivel de sistema operativo en su implementación (u otras herramientas asincrónicas a nivel de sistema operativo).
  4. Construya sobre API asincrónicas previamente existentes y no tenga ningún código propio que demore mucho en el hilo principal.
jfriend00 avatar Dec 20 '2018 22:12 jfriend00

La función ejecutora de una promesa se ejecuta de forma sincrónica y es por eso que su código bloquea el hilo principal de ejecución.

Para no bloquear el hilo principal de ejecución, es necesario ceder el control de forma periódica y cooperativa mientras se realiza la tarea de larga duración. De hecho, debe dividir la tarea en subtareas y luego coordinar la ejecución de las subtareas en nuevos ticks del bucle de eventos. De esta manera, le da a otras tareas (como renderizar y responder a las entradas del usuario) la oportunidad de ejecutarse.

Puede escribir su propio bucle asíncrono utilizando la API de promesa o puede utilizar una función asíncrona. Las funciones asíncronas permiten la suspensión y reanudación de funciones (reentrada) y le ocultan la mayor parte de la complejidad.

El siguiente código se utiliza setTimeoutpara mover subtareas a nuevos ticks de bucle de eventos. Por supuesto, esto podría generalizarse y se podría utilizar el procesamiento por lotes para encontrar un equilibrio entre el progreso de la tarea y la capacidad de respuesta de la interfaz de usuario; El tamaño del lote en esta solución es solo 1, por lo que el progreso es lento.

Finalmente: la verdadera solución a este tipo de problemas es probablemente un Worker .

const $ = document.querySelector.bind(document)
const BIG_NUMBER = 1000
let count = 0

// Note that this could also use requestIdleCallback or requestAnimationFrame
const tick = (fn) => new Promise((resolve) => setTimeout(() => resolve(fn), 5))

async function longRunningTask(){
    while (count++ < BIG_NUMBER) await tick()
    console.log(`A big number of loops done.`)
}

console.log(`*** STARTING ***`)
longRunningTask().then(() => console.log(`*** COMPLETED ***`))
$('button').onclick = () => $('#output').innerHTML += `Current count is: ${count}<br/>`
* {
  font-size: 16pt;
  color: gray;
  padding: 15px;
}
<button>Click me to see that the UI is still responsive.</button>
<div id="output"></div>
Expandir fragmento

Ben Aston avatar May 06 '2020 15:05 Ben Aston