¿Cómo envolver las llamadas a funciones asíncronas en una función de sincronización en Node.js o Javascript?

122

Suponga que mantiene una biblioteca que expone una función getData. Sus usuarios lo llaman para obtener datos reales:
var output = getData();
los datos internos se guardan en un archivo para que los implemente getDatautilizando Node.js integrado fs.readFileSync. Es obvio tanto getDatay fs.readFileSyncson funciones de sincronización. Un día le dijeron que cambiara la fuente de datos subyacente a un repositorio como MongoDB, al que solo se puede acceder de forma asincrónica. También se le dijo que evitara enojar a sus usuarios, la getDataAPI no se puede cambiar para devolver simplemente una promesa o exigir un parámetro de devolución de llamada. ¿Cómo cumple ambos requisitos?

La función asincrónica que usa callback / promise es el ADN de JavasSript y Node.js. Cualquier aplicación JS no trivial probablemente esté impregnada de este estilo de codificación. Pero esta práctica puede conducir fácilmente a la llamada pirámide de la fatalidad. Peor aún, si algún código en cualquier llamador en la cadena de llamadas depende del resultado de la función asíncrona, ese código también debe estar incluido en la función de devolución de llamada, imponiendo una restricción de estilo de codificación al llamador. De vez en cuando, encuentro la necesidad de encapsular una función asíncrona (a menudo proporcionada en una biblioteca de terceros) en una función de sincronización para evitar una refactorización global masiva. La búsqueda de una solución sobre este tema generalmente terminaba con Node Fiberso paquetes npm derivados de él. Pero las fibras simplemente no pueden resolver el problema al que me enfrento. Incluso el ejemplo proporcionado por el autor de Fibers ilustra la deficiencia:

...
Fiber(function() {
    console.log('wait... ' + new Date);
    sleep(1000);
    console.log('ok... ' + new Date);
}).run();
console.log('back in main');

Salida real:

wait... Fri Jan 21 2011 22:42:04 GMT+0900 (JST)
back in main
ok... Fri Jan 21 2011 22:42:05 GMT+0900 (JST)

Si la función Fiber realmente convierte la función asíncrona en reposo en sincronizada, la salida debería ser:

wait... Fri Jan 21 2011 22:42:04 GMT+0900 (JST)
ok... Fri Jan 21 2011 22:42:05 GMT+0900 (JST)
back in main

Creé otro ejemplo simple en JSFiddle y busqué código para producir el resultado esperado. Aceptaré una solución que solo funcione en Node.js, por lo que puede solicitar cualquier paquete npm a pesar de no funcionar en JSFiddle.

abbr
fuente
2
Las funciones asincrónicas nunca pueden sincronizarse en Node, e incluso si pudieran, usted no debería hacerlo. El problema es tal que en el módulo fs puede ver funciones completamente separadas para el acceso sincrónico y asincrónico al sistema de archivos. Lo mejor que puede hacer es enmascarar la apariencia de async con promesas o corrutinas (generadores en ES6). Para administrar pirámides de devolución de llamada, déles nombres en lugar de definirlos en una llamada de función y use algo como la biblioteca asíncrona.
qubyte
8
Para dandavis, async agrega detalles de implementación a la cadena de llamadas, lo que a veces obliga a la refactorización global. Esto es perjudicial e incluso desastroso para una aplicación compleja donde la modularización y la contención son importantes.
abbr
4
"Callback pyramid of doom" es solo la representación del problema. Promise puede ocultarlo o disfrazarlo, pero no puede abordar el verdadero desafío: si el llamador de una función asíncrona depende de los resultados de la función asíncrona, tiene que usar la devolución de llamada, y también lo hace su llamador, etc. Este es un ejemplo clásico de imponer restricciones a llamador simplemente por los detalles de implementación.
abbr
1
@abbr: Gracias por el módulo deasync, la descripción de su problema es exactamente lo que estaba buscando y no pude encontrar ninguna solución viable. Jugué con generadores e iterables, pero llegué a las mismas conclusiones que tú.
Kevin Jhangiani
2
Vale la pena señalar que casi nunca es una buena idea forzar la sincronización de una función asíncrona. Usted casi siempre tiene una mejor solución que mantiene la asíncrono-dad de la función intacta, sin dejar de lograr el mismo efecto (como la secuenciación, la variable de ajuste, etc).
Madara's Ghost

Respuestas:

104

deasync convierte la función asíncrona en sincronización, implementada con un mecanismo de bloqueo al llamar al bucle de eventos Node.js en la capa de JavaScript. Como resultado, deasync solo bloquea el código subsiguiente para que no se ejecute sin bloquear todo el hilo, ni provocar una espera ocupada. Con este módulo, aquí está la respuesta al desafío jsFiddle:

function AnticipatedSyncFunction(){
  var ret;
  setTimeout(function(){
      ret = "hello";
  },3000);
  while(ret === undefined) {
    require('deasync').runLoopOnce();
  }
  return ret;    
}


var output = AnticipatedSyncFunction();
//expected: output=hello (after waiting for 3 sec)
console.log("output="+output);
//actual: output=hello (after waiting for 3 sec)

(descargo de responsabilidad: soy coautor de deasync. El módulo se creó después de publicar esta pregunta y no se encontró una propuesta viable).

abbr
fuente
¿Alguien más tuvo suerte con esto? No puedo hacer que funcione.
Newman
3
No puedo hacer que funcione correctamente. debería mejorar su documentación para este módulo, si desea que se utilice más. Dudo que los autores sepan exactamente cuáles son las ramificaciones del uso del módulo, y si lo hacen, ciertamente no las documentan.
Alexander Mills
5
Hasta ahora, hay un problema confirmado documentado en el rastreador de problemas de github. El problema se ha solucionado en Node v0.12. El resto que conozco son meras especulaciones infundadas que no vale la pena documentar. Si cree que su problema es causado por deasync, publique un escenario duplicable e independiente y lo analizaré.
abbr
Intenté usarlo y obtuve algunas mejoras en mi script, pero aún así no tuve suerte con la fecha. Modifiqué el código de la siguiente manera: ¡ function AnticipatedSyncFunction(){ var ret; setTimeout(function(){ var startdate = new Date() //console.log(startdate) ret = "hello" + startdate; },3000); while(ret === undefined) { require('deasync').runLoopOnce(); } return ret; } var output = AnticipatedSyncFunction(); var startdate = new Date() console.log(startdate) console.log("output="+output); y espero ver 3 segundos de diferencia en la salida de fecha!
Alex
@abbr se puede navegar y usar sin dependencia de nodo>
Gandhi
5

También hay un módulo de sincronización npm. que se utiliza para sincronizar el proceso de ejecución de la consulta.

Cuando desee ejecutar consultas paralelas de forma síncrona, entonces el nodo restringe para hacerlo porque nunca espera una respuesta. y el módulo de sincronización es perfecto para ese tipo de solución.

Código de muestra

/*require sync module*/
var Sync = require('sync');
    app.get('/',function(req,res,next){
      story.find().exec(function(err,data){
        var sync_function_data = find_user.sync(null, {name: "sanjeev"});
          res.send({story:data,user:sync_function_data});
        });
    });


    /*****sync function defined here *******/
    function find_user(req_json, callback) {
        process.nextTick(function () {

            users.find(req_json,function (err,data)
            {
                if (!err) {
                    callback(null, data);
                } else {
                    callback(null, err);
                }
            });
        });
    }

enlace de referencia: https://www.npmjs.com/package/sync

sanjeev kumar
fuente
4

Si la función de fibra realmente convierte la función asíncrona en reposo en sincronización

Si. Dentro de la fibra, la función espera antes de iniciar sesión ok. Las fibras no hacen que las funciones asíncronas sean síncronas, pero permiten escribir código de apariencia síncrona que usa funciones asíncronas y luego se ejecutará de forma asíncrona dentro de un archivo Fiber.

De vez en cuando encuentro la necesidad de encapsular una función asíncrona en una función de sincronización para evitar una refactorización global masiva.

No puedes. Es imposible sincronizar el código asincrónico. Deberá anticipar eso en su código global y escribirlo en estilo asíncrono desde el principio. Ya sea que envuelva el código global en una fibra, use promesas, generadores de promesas o devoluciones de llamada simples, depende de sus preferencias.

Mi objetivo es minimizar el impacto en la persona que llama cuando el método de adquisición de datos se cambia de sincronizado a asincrónico

Tanto las promesas como las fibras pueden hacer eso.

Bergi
fuente
1
esto es lo peor ABSOLUTO que puede hacer con Node.js: "código de aspecto sincrónico que usa funciones asíncronas y luego se ejecutará de manera asincrónica". si su API hace eso, arruinará vidas. si es asincrónico, debería requerir una devolución de llamada y arrojar un error si no se proporciona ninguna devolución de llamada. esa es la mejor manera de crear una API, a menos que su objetivo sea engañar a la gente.
Alexander Mills
@AlexMills: Sí, eso sería realmente horrible . Sin embargo, afortunadamente, esto no es nada que pueda hacer una API. Una API asincrónica siempre necesita aceptar una devolución de llamada / devolver una promesa / esperar que se ejecute dentro de una fibra; no funciona sin ella. Afaik, las fibras se usaban principalmente en scripts rápidos y sucios que estaban bloqueando y no tenían ninguna concurrencia, pero querían usar API asíncronas; al igual que en el nodo, a veces hay casos en los que usarías los fsmétodos síncronos .
Bergi
2
Generalmente me gusta node. Especialmente si puedo usar mecanografiado en lugar de js puro. Pero toda esta tontería asincrónica que impregna todo lo que haces y literalmente infecta cada función en la cadena de llamadas tan pronto como decides hacer una sola llamada asíncrona es algo que realmente ... realmente odio. Async api es como una enfermedad infecciosa, una llamada infecta toda la base de su código y lo obliga a reescribir todo el código que tiene. Realmente no entiendo cómo alguien puede argumentar que esto es algo bueno .
Kris
@Kris Node usa un modelo asíncrono para tareas de IO porque es rápido y simple. También puede hacer muchas cosas sincrónicamente, pero el bloqueo es lento ya que no puede hacer nada al mismo tiempo, a menos que elija hilos, lo que hace que todo sea complicado.
Bergi
@Bergi Leí el manifiesto, así que conozco los argumentos. Pero cambiar su código existente a asíncrono en el momento en que ingresa a la primera llamada a la API que no tiene equivalente de sincronización no es simple. Todo se rompe y cada línea de código debe ser analizada. A menos que su código sea trivial, lo garantizo ... llevará un tiempo convertirlo y hacerlo funcionar nuevamente después de convertir todo al lenguaje asincrónico.
Kris
2

Tienes que usar promesas:

const asyncOperation = () => {
    return new Promise((resolve, reject) => {
        setTimeout(()=>{resolve("hi")}, 3000)
    })
}

const asyncFunction = async () => {
    return await asyncOperation();
}

const topDog = () => {
    asyncFunction().then((res) => {
        console.log(res);
    });
}

Me gustan más las definiciones de funciones de flecha. Pero cualquier cadena de la forma "() => {...}" también podría escribirse como "función () {...}"

Entonces, topDog no es asíncrono a pesar de llamar a una función asíncrona.

ingrese la descripción de la imagen aquí

EDITAR: Me doy cuenta de que muchas de las veces que necesita para envolver una función asíncrona dentro de una función de sincronización está dentro de un controlador. Para esas situaciones, aquí hay un truco de fiesta:

const getDemSweetDataz = (req, res) => {
    (async () => {
        try{
            res.status(200).json(
                await asyncOperation()
            );
        }
        catch(e){
            res.status(500).json(serviceResponse); //or whatever
        }
    })() //So we defined and immediately called this async function.
}

Utilizando esto con devoluciones de llamada, puede hacer un ajuste que no use promesas:

const asyncOperation = () => {
    return new Promise((resolve, reject) => {
        setTimeout(()=>{resolve("hi")}, 3000)
    })
}

const asyncFunction = async (callback) => {
    let res = await asyncOperation();
    callback(res);
}

const topDog = () => {
    let callback = (res) => {
        console.log(res);
    };

    (async () => {
        await asyncFunction(callback)
    })()
}

Al aplicar este truco a un EventEmitter, puede obtener los mismos resultados. Defina el oyente de EventEmitter donde definí la devolución de llamada y emita el evento donde llamé a la devolución de llamada.

usuario2485309
fuente
1

No puedo encontrar un escenario que no se pueda resolver usando fibras de nodo. El ejemplo que proporcionó utilizando fibras de nodo se comporta como se esperaba. La clave es ejecutar todo el código relevante dentro de una fibra, para que no tenga que iniciar una nueva fibra en posiciones aleatorias.

Veamos un ejemplo: digamos que usa algún marco, que es el punto de entrada de su aplicación (no puede modificar este marco). Este marco carga módulos de nodejs como complementos y llama a algunos métodos en los complementos. Digamos que este marco solo acepta funciones sincrónicas y no utiliza fibras por sí mismo.

Hay una biblioteca que desea usar en uno de sus complementos, pero esta biblioteca es asincrónica y tampoco desea modificarla.

El hilo principal no se puede ceder cuando no se está ejecutando fibra, ¡pero aún puede crear complementos utilizando fibras! Simplemente cree una entrada de envoltura que inicie todo el marco dentro de una fibra, para que pueda generar la ejecución de los complementos.

Desventaja: si el marco usa setTimeouto Promises internamente, escapará del contexto de la fibra. Esto se puede evitar al burlarse setTimeout, Promise.theny todos los controladores de eventos.

Así es como se puede producir una fibra hasta que Promisese resuelva a. Este código toma una función asincrónica (devolución de promesa) y reanuda la fibra cuando se resuelve la promesa:

framework-entry.js

console.log(require("./my-plugin").run());

async-lib.js

exports.getValueAsync = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Async Value");
    }, 100);
  });
};

my-plugin.js

const Fiber = require("fibers");

function fiberWaitFor(promiseOrValue) {
  var fiber = Fiber.current, error, value;
  Promise.resolve(promiseOrValue).then(v => {
    error = false;
    value = v;
    fiber.run();
  }, e => {
    error = true;
    value = e;
    fiber.run();
  });
  Fiber.yield();
  if (error) {
    throw value;
  } else {
    return value;
  }
}

const asyncLib = require("./async-lib");

exports.run = () => {
  return fiberWaitFor(asyncLib.getValueAsync());
};

my-entry.js

require("fibers")(() => {
  require("./framework-entry");
}).run();

Cuando se ejecuta node framework-entry.jsse generará un error: Error: yield() called with no fiber running. Si lo ejecuta node my-entry.js, funciona como se esperaba.

Tamas Hegedus
fuente
0

Hacer que el código de Node.js se sincronice es esencial en algunos aspectos, como la base de datos. Pero la ventaja real de Node.js radica en el código asincrónico. Como es de un solo hilo sin bloqueo.

podemos sincronizarlo usando una funcionalidad importante Fiber () Use await () y defer () llamamos a todos los métodos usando await (). luego reemplace las funciones de devolución de llamada con defer ().

Código asincrónico normal: utiliza funciones de CallBack.

function add (var a, var b, function(err,res){
       console.log(res);
});

 function sub (var res2, var b, function(err,res1){
           console.log(res);
    });

 function div (var res2, var b, function(err,res3){
           console.log(res3);
    });

Sincronice el código anterior usando Fiber (), await () y defer ()

fiber(function(){
     var obj1 = await(function add(var a, var b,defer()));
     var obj2 = await(function sub(var obj1, var b, defer()));
     var obj3 = await(function sub(var obj2, var b, defer()));

});

Espero que esto sea de ayuda. Gracias

Mohan Ramakrishna
fuente
0

Hoy en día este patrón generador puede ser una solución en muchas situaciones.

Aquí un ejemplo de indicaciones de consola secuenciales en nodejs usando la función async readline.question:

var main = (function* () {

  // just import and initialize 'readline' in nodejs
  var r = require('readline')
  var rl = r.createInterface({input: process.stdin, output: process.stdout })

  // magic here, the callback is the iterator.next
  var answerA = yield rl.question('do you want this? ', r=>main.next(r))    

  // and again, in a sync fashion
  var answerB = yield rl.question('are you sure? ', r=>main.next(r))        

  // readline boilerplate
  rl.close()

  console.log(answerA, answerB)

})()  // <-- executed: iterator created from generator
main.next()     // kick off the iterator, 
                // runs until the first 'yield', including rightmost code
                // and waits until another main.next() happens
drodsou
fuente
-1

No debería estar mirando lo que sucede alrededor de la llamada que crea la fibra, sino más bien lo que sucede dentro de la fibra. Una vez que esté dentro de la fibra, puede programar en estilo sincronizado. Por ejemplo:

función f1 () {
    console.log ('espera ...' + nueva fecha);
    dormir (1000);
    console.log ('ok ...' + nueva fecha);   
}

función f2 () {
    f1 ();
    f1 ();
}

Fibra (función () {
    f2 ();
}).correr();

Dentro de la fibra que llamas f1, f2y sleepcomo si estuvieran sincronizados.

En una aplicación web típica, creará la fibra en su despachador de solicitudes HTTP. Una vez que haya hecho eso, puede escribir toda la lógica de manejo de solicitudes en estilo de sincronización, incluso si llama a funciones asíncronas (fs, bases de datos, etc.).

Bruno Jouhier
fuente
Gracias Bruno. Pero, ¿qué sucede si necesito un estilo de sincronización en el código de arranque que debe ejecutarse antes de que el servidor se una al puerto tcp, como la configuración o los datos que deben leerse desde la base de datos que se abre de forma asíncrona? Es posible que termine envolviendo server.js completo en Fiber, y sospecho que eso matará la concurrencia en todo el nivel del proceso. No obstante, es una sugerencia que vale la pena verificar. Para mí, la solución ideal debería poder envolver una función asíncrona para proporcionar una sintaxis de llamada de sincronización y solo bloquear las siguientes líneas de código en la cadena de llamadas sin sacrificar la concurrencia a nivel de proceso.
abril
Puede envolver todo su código de arranque dentro de una gran llamada de Fiber. La simultaneidad no debería ser un problema porque el código de arranque generalmente necesita ejecutarse hasta su finalización antes de comenzar a atender las solicitudes. Además, una fibra no evita que se corran otras fibras: cada vez que se alcanza un límite de rendimiento, se le da a otras fibras (y al hilo principal) la oportunidad de correr.
Bruno Jouhier
Envolví el archivo de arranque Express server.js con fibra. La secuencia de ejecución es lo que estoy buscando, pero ese ajuste no tiene ningún efecto en el controlador de solicitudes. Así que supongo que tengo que aplicar la misma envoltura a CADA despachador. Me di por vencido en este punto porque no parece que sea mejor para ayudar a evitar la refactorización global. Mi objetivo es minimizar el impacto en la persona que llama cuando el método de adquisición de datos se cambia de sincronizado a asincrónico en la capa DAO y Fiber todavía se queda un poco corta para el desafío.
abril
@fred: No tiene mucho sentido "sincronizar" flujos de eventos como el controlador de solicitudes; necesitaría tener un while(true) handleNextRequest()bucle. Envolver cada gestor de solicitudes en una fibra lo haría.
Bergi
@fred: las fibras no le ayudarán mucho con Express porque la devolución de llamada de Express no es una devolución de llamada de continuación (una devolución de llamada que siempre se llama exactamente una vez, ya sea con un error o con un resultado). Pero las fibras resolverán la pirámide de la fatalidad cuando tenga mucho código escrito en la parte superior de las API asíncronas con devoluciones de llamada de continuación (como fs, mongodb y muchos otros).
Bruno Jouhier
-2

Al principio luché con esto con node.js y async.js es la mejor biblioteca que he encontrado para ayudarlo a lidiar con esto. Si desea escribir código síncrono con el nodo, el enfoque es de esta manera.

var async = require('async');

console.log('in main');

doABunchOfThings(function() {
  console.log('back in main');
});

function doABunchOfThings(fnCallback) {
  async.series([
    function(callback) {
      console.log('step 1');
      callback();
    },
    function(callback) {
      setTimeout(callback, 1000);
    },
    function(callback) {
      console.log('step 2');
      callback();
    },
    function(callback) {
      setTimeout(callback, 2000);
    },
    function(callback) {
      console.log('step 3');
      callback();
    },
  ], function(err, results) {
    console.log('done with things');
    fnCallback();
  });
}

este programa SIEMPRE producirá lo siguiente ...

in main
step 1
step 2
step 3
done with things
back in main
Michael Connor
fuente
2
asyncfunciona en su ejemplo porque es main, que no se preocupa por la persona que llama. Imagine que todo su código está envuelto en una función que se supone que devuelve el resultado de una de sus llamadas a funciones asíncronas. Se puede comprobar fácilmente que no funciona agregando console.log('return');al final de su código. En tal caso, la salida de returnocurrirá después in mainpero antes step 1.
abbr
-11

Javascript es un lenguaje de un solo hilo, ¡no quieres bloquear todo tu servidor! El código asincrónico elimina las condiciones de carrera al hacer explícitas las dependencias.

¡Aprenda a amar el código asincrónico!

Eche un vistazo al promisescódigo asincrónico sin crear una pirámide de infierno de devolución de llamada. Recomiendo la biblioteca promiseQ para node.js

httpGet(url.parse("http://example.org/")).then(function (res) {
    console.log(res.statusCode);  // maybe 302
    return httpGet(url.parse(res.headers["location"]));
}).then(function (res) {
    console.log(res.statusCode);  // maybe 200
});

http://howtonode.org/promises

EDITAR: esta es, con mucho, mi respuesta más controvertida, el nodo ahora tiene la palabra clave yield, que le permite tratar el código asíncrono como si fuera síncrono. http://blog.alexmaccaw.com/how-yield-will-transform-node

roo2
fuente
1
Promise solo reformula un parámetro de devolución de llamada en lugar de convertir la función en sincronización.
abbr
2
¡No quiere que se sincronice o se bloqueará todo su servidor! stackoverflow.com/questions/17959663/…
roo2
1
Lo deseable es una llamada de sincronización sin bloquear otros eventos, como otra solicitud manejada por Node.js. Una función de sincronización, por definición, solo significa que no volverá a la persona que llama hasta que se produzca el resultado (no simplemente una promesa). No excluye previamente al servidor de manejar otros eventos mientras la llamada está bloqueada.
abbr
@fred: Creo que estás perdiendo el sentido de las promesas . No son simplemente una abstracción de patrones del observador, sino que proporcionan una forma de encadenar y componer acciones asincrónicas.
Bergi
1
@Bergi, uso mucho la promesa y sé exactamente lo que hace. Efectivamente, todo lo que logró es dividir una única invocación de función asíncrona en múltiples invocaciones / declaraciones. Pero no cambia el resultado: cuando la persona que llama regresa, no puede devolver el resultado de la función asíncrona. Mira el ejemplo que publiqué en JSFiddle. La persona que llama en ese caso es la función AnticipatedSyncFunction y la función asíncrona es setTimeout. Si puedes responder a mi desafío usando la promesa, por favor muéstramelo.
abril