Come avvolgere le chiamate di funzioni asincrone in una funzione di sincronizzazione in Node.js o Javascript?


122

Supponi di mantenere una libreria che espone una funzione getData. Gli utenti chiamano per ottenere dati reali:
var output = getData();
Sotto il cofano i dati vengono salvati in un file in modo da implementato getDatautilizzando Node.js built-in fs.readFileSync. È ovvio sia getDatae fs.readFileSyncsono funzioni di sincronizzazione. Un giorno ti è stato detto di cambiare l'origine dati sottostante in un repository come MongoDB a cui è possibile accedere solo in modo asincrono. Ti è stato anche detto di evitare di far incazzare i tuoi utenti, l' getDataAPI non può essere modificata per restituire semplicemente una promessa o richiedere un parametro di callback. Come soddisfi entrambi i requisiti?

La funzione asincrona che utilizza callback / promise è il DNA di JavasSript e Node.js. Qualsiasi app JS non banale è probabilmente permeata di questo stile di codifica. Ma questa pratica può facilmente portare alla cosiddetta piramide del richiamo del destino. Ancora peggio, se qualsiasi codice in un chiamante nella catena di chiamate dipende dal risultato della funzione asincrona, anche quel codice deve essere avvolto nella funzione di callback, imponendo un vincolo di stile di codifica al chiamante. Di tanto in tanto trovo la necessità di incapsulare una funzione asincrona (spesso fornita in una libreria di terze parti) in una funzione di sincronizzazione al fine di evitare un massiccio rifactoring globale. La ricerca di una soluzione su questo argomento di solito si è conclusa con Node Fibreso pacchetti npm derivati ​​da esso. Ma le fibre non possono risolvere il problema che sto affrontando. Anche l'esempio fornito dall'autore di Fibers ha illustrato la carenza:

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

Uscita effettiva:

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)

Se la funzione Fiber trasforma davvero la funzione sleep async in sync, l'output dovrebbe essere:

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

Ho creato un altro semplice esempio in JSFiddle e sto cercando il codice per produrre l'output previsto. Accetterò una soluzione che funziona solo in Node.js quindi sei libero di richiedere qualsiasi pacchetto npm nonostante non funzioni in JSFiddle.


2
Le funzioni asincrone non possono mai essere rese sincrone in Node, e anche se potessero, non dovresti. Il problema è tale che nel modulo fs puoi vedere funzioni completamente separate per l'accesso sincrono e asincrono al file system. Il meglio che puoi fare è mascherare l'aspetto di asincrono con promesse o coroutine (generatori in ES6). Per gestire le piramidi di callback, assegna loro dei nomi invece di definirli in una chiamata di funzione e usa qualcosa come la libreria asincrona.
qubyte

8
Per dandavis, async espone i dettagli di implementazione alla catena di chiamate, a volte forzando il refactoring globale. Ciò è dannoso e persino disastroso per un'applicazione complessa in cui la modularizzazione e il contenimento sono importanti.
abbr

4
"Callback pyramid of doom" è solo la rappresentazione del problema. Promise può nasconderlo o camuffarlo ma non può affrontare la vera sfida: se il chiamante di una funzione asincrona dipende dai risultati della funzione asincrona, deve usare la richiamata, e così fa il suo chiamante ecc. Questo è un classico esempio di imposizione di vincoli a chiamante semplicemente a causa dei dettagli di implementazione.
abbr

1
@abbr: Grazie per il modulo deasync, la descrizione del tuo problema è esattamente quello che stavo cercando e non sono riuscito a trovare soluzioni praticabili. Ho scherzato con generatori e iterabili, ma sono giunto alle tue stesse conclusioni.
Kevin Jhangiani

2
Vale la pena notare che non è quasi mai una buona idea forzare la sincronizzazione di una funzione asincrona. Hai quasi sempre una soluzione migliore che mantiene intatta l'asincronia della funzione, pur ottenendo lo stesso effetto (come la sequenza, l'impostazione delle variabili, ecc.).
Il fantasma di Madara

Risposte:


104

deasync trasforma la funzione async in sync, implementata con un meccanismo di blocco chiamando il loop di eventi Node.js a livello JavaScript. Di conseguenza, deasync blocca solo l'esecuzione del codice successivo senza bloccare l'intero thread, né incorrere in un'attesa occupata. Con questo modulo, ecco la risposta alla sfida 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)

(Dichiarazione di non responsabilità: sono il coautore di deasync. Il modulo è stato creato dopo aver pubblicato questa domanda e non ha trovato alcuna proposta realizzabile.)


Qualcun altro ha avuto fortuna con questo? Non riesco a farlo funzionare.
newman

3
Non riesco a farlo funzionare correttamente. dovresti migliorare la tua documentazione per questo modulo, se desideri che venga utilizzato di più. Dubito che gli autori sappiano esattamente quali sono le ramificazioni per l'utilizzo del modulo, e se lo fanno, certamente non le documentano.
Alexander Mills

5
Finora c'è un problema confermato documentato nel tracker dei problemi di GitHub. Il problema è stato risolto in Node v0.12. Il resto che so sono solo speculazioni infondate che non vale la pena documentare. Se ritieni che il tuo problema sia causato da deasync, pubblica uno scenario autonomo e duplicabile e io esaminerò.
abbr

Ho provato a usarlo e ho ottenuto alcuni miglioramenti nel mio script ma ancora non ho avuto fortuna con la data. Ho modificato il codice come segue: 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); e mi aspetto di vedere 3 secondi di diverso nell'output della data!
Alex

@abbr può essere navigato e utilizzato senza dipendenza dal nodo>
Gandhi

5

C'è anche un modulo di sincronizzazione npm. che viene utilizzato per sincronizzare il processo di esecuzione della query.

Quando si desidera eseguire query parallele in modo sincrono, il nodo si limita a farlo perché non attende mai la risposta. e il modulo di sincronizzazione è molto perfetto per quel tipo di soluzione.

Codice di esempio

/*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);
                }
            });
        });
    }

link di riferimento: https://www.npmjs.com/package/sync


4

If function Fiber trasforma davvero la funzione sleep async in sync

Sì. All'interno della fibra, la funzione attende prima di registrarsi ok. Le fibre non rendono sincrone le funzioni asincrone, ma consentono di scrivere codice dall'aspetto sincrono che utilizza funzioni asincrone e quindi verrà eseguito in modo asincrono all'interno di un file Fiber.

Di tanto in tanto trovo la necessità di incapsulare una funzione asincrona in una funzione di sincronizzazione per evitare un massiccio rifattoraggio globale.

Non puoi. È impossibile rendere sincrono il codice asincrono. Dovrai anticiparlo nel tuo codice globale e scriverlo in stile asincrono dall'inizio. Se avvolgi il codice globale in una fibra, usi promesse, generatori di promesse o semplici callback dipende dalle tue preferenze.

Il mio obiettivo è ridurre al minimo l'impatto sul chiamante quando il metodo di acquisizione dei dati viene modificato da sincronizzazione ad asincrona

Sia le promesse che le fibre possono farlo.


1
questa è la cosa peggiore in assoluto che puoi fare con Node.js: "codice dall'aspetto sincrono che utilizza funzioni asincrone e quindi verrà eseguito in modo asincrono." se la tua API lo fa, rovinerai delle vite. se è asincrono, dovrebbe richiedere un callback e generare un errore se non viene fornito alcun callback. questo è il modo migliore per creare un'API, a meno che il tuo obiettivo non sia ingannare le persone.
Alexander Mills

@AlexMills: Sì, sarebbe davvero orribile . Tuttavia, fortunatamente questo non è nulla che un'API possa fare. Un'API asincrona deve sempre accettare una richiamata / restituire una promessa / aspettarsi di essere eseguita all'interno di una fibra - non funziona senza. Inoltre, le fibre sono state utilizzate principalmente in script quick'n'dirty che bloccavano e non hanno alcuna concorrenza, ma vogliono usare API asincrone; proprio come nel nodo, a volte ci sono casi in cui useresti i fsmetodi sincroni .
Bergi

2
In genere mi piace il nodo. Soprattutto se posso usare il dattiloscritto invece di puro js. Ma tutta questa assurdità asincrona che permea tutto ciò che fai e infetta letteralmente ogni funzione nella catena di chiamate non appena decidi di effettuare una singola chiamata asincrona è qualcosa che davvero ... davvero odio. L'api Async è come una malattia infettiva, una chiamata infetta l'intera base di codice costringendoti a riscrivere tutto il codice che hai. Non capisco davvero come qualcuno possa sostenere che questa sia una buona cosa.
Kris

@Kris Node utilizza un modello asincrono per le attività di I / O perché è veloce e semplice. Puoi anche fare molte cose in modo sincrono, ma il blocco è lento in quanto non puoi fare nulla contemporaneamente, a meno che tu non scelga i thread, il che rende tutto complicato.
Bergi

@Bergi ho letto il manifesto quindi conosco gli argomenti. Ma cambiare il codice esistente in asincrono nel momento in cui premi la prima chiamata api che non ha un equivalente di sincronizzazione non è semplice. Tutto si rompe e ogni singola riga di codice deve essere esaminata. A meno che il tuo codice non sia banale, ti garantisco ... ci vorrà un po 'per convertirlo e farlo funzionare di nuovo dopo aver convertito l'intera cosa in linguaggio asincrono.
Kris

2

Devi usare le promesse:

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);
    });
}

Mi piacciono di più le definizioni delle funzioni freccia. Ma qualsiasi stringa della forma "() => {...}" potrebbe anche essere scritta come "funzione () {...}"

Quindi topDog non è asincrono nonostante abbia chiamato una funzione asincrona.

inserisci qui la descrizione dell'immagine

EDIT: Mi rendo conto che molte delle volte in cui è necessario avvolgere una funzione asincrona all'interno di una funzione di sincronizzazione è all'interno di un controller. Per quelle situazioni, ecco un trucco da festa:

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.
}

Utilizzando questo con i callback, puoi fare un wrap che non usa le promesse:

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)
    })()
}

Applicando questo trucco a un EventEmitter, puoi ottenere gli stessi risultati. Definisci il listener di EventEmitter in cui ho definito il callback ed emetti l'evento in cui ho chiamato il callback.


1

Non riesco a trovare uno scenario che non possa essere risolto utilizzando le fibre del nodo. L'esempio fornito utilizzando le fibre del nodo si comporta come previsto. La chiave è eseguire tutto il codice rilevante all'interno di una fibra, in modo da non dover avviare una nuova fibra in posizioni casuali.

Vediamo un esempio: supponiamo che tu usi un framework, che è il punto di ingresso della tua applicazione (non puoi modificare questo framework). Questo framework carica i moduli nodejs come plugin e chiama alcuni metodi sui plugin. Diciamo che questo framework accetta solo funzioni sincrone e non utilizza le fibre da solo.

C'è una libreria che vuoi usare in uno dei tuoi plugin, ma questa libreria è asincrona e non vuoi nemmeno modificarla.

Il thread principale non può essere ceduto quando nessuna fibra è in esecuzione, ma puoi comunque creare plugin usando le fibre! Basta creare una voce wrapper che avvia l'intero framework all'interno di una fibra, in modo da poter ottenere l'esecuzione dai plugin.

Svantaggio: se il framework utilizza setTimeouto Promises internamente, sfuggirà al contesto della fibra. Questo può essere svolte intorno beffardo setTimeout, Promise.thene tutti i gestori di eventi.

Quindi è così che puoi produrre una fibra fino a quando non Promiseviene risolto. Questo codice accetta una funzione asincrona (ritorno della promessa) e riprende la fibra quando la promessa viene risolta:

-quadro entry.js

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

asincrone-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();

Quando si esegue node framework-entry.jslo genera un errore: Error: yield() called with no fiber running. Se lo esegui node my-entry.jsfunziona come previsto.


0

La sincronizzazione del codice Node.js è essenziale in pochi aspetti come il database. Ma il vero vantaggio di Node.js risiede nel codice asincrono. Poiché è un thread singolo non bloccante.

possiamo sincronizzarlo usando la funzionalità importante Fiber (). Usa await () e defer (), chiamiamo tutti i metodi usando await (). quindi sostituire le funzioni di callback con defer ().

Normale codice asincrono, che utilizza le funzioni 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);
    });

Sincronizza il codice sopra usando Fiber (), await () e 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()));

});

Spero che questo possa aiutare. Grazie


0

Al giorno d'oggi questo modello generatore può essere una soluzione in molte situazioni.

Ecco un esempio di prompt di console sequenziali in nodejs utilizzando la funzione readline.question asincrona:

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

-1

Non dovresti guardare a ciò che accade intorno alla chiamata che crea la fibra, ma piuttosto a ciò che accade all'interno della fibra. Una volta che sei all'interno della fibra puoi programmare in stile sync. Per esempio:

funzione f1 () {
    console.log ('wait ...' + new Date);
    sleep (1000);
    console.log ('ok ...' + nuova data);   
}

funzione f2 () {
    f1 ();
    f1 ();
}

Fibra (function () {
    f2 ();
}).correre();

Dentro la fibra si chiama f1, f2e sleepcome se fossero sincronizzati.

In una tipica applicazione web, creerai la fibra nel tuo dispatcher di richieste HTTP. Dopo averlo fatto, puoi scrivere tutta la logica di gestione delle richieste in stile sync, anche se chiama funzioni async (fs, database, ecc.).


Grazie Bruno. Ma cosa succede se ho bisogno di uno stile di sincronizzazione nel codice bootstrap che deve essere eseguito prima che il server si colleghi alla porta tcp, come la configurazione oi dati che devono essere letti dal db che viene aperto in modo asincrono? Potrei finire con il wrapping dell'intero server.js in Fiber e sospetto che ucciderà la concorrenza a livello dell'intero processo. Tuttavia è un suggerimento che vale la pena verificare. Per me la soluzione ideale dovrebbe essere in grado di racchiudere una funzione asincrona per fornire una sintassi di chiamata di sincronizzazione e bloccare solo le righe di codice successive nella catena del chiamante senza sacrificare la concorrenza a livello di processo.
abbr

Puoi racchiudere l'intero codice bootstrap in un'unica grande chiamata Fiber. La concorrenza non dovrebbe essere un problema perché il codice bootstrap di solito deve essere eseguito fino al completamento prima di iniziare a servire le richieste. Inoltre, una fibra non impedisce ad altre fibre di scorrere: ogni volta che ottieni una chiamata di rendimento, dai ad altre fibre (e al filo principale) la possibilità di correre.
Bruno Jouhier

Ho avvolto Express file bootstrap server.js con fibra. La sequenza di esecuzione è quello che sto cercando, ma quell'involucro non ha alcun effetto sul gestore delle richieste. Quindi immagino di dover applicare lo stesso wrapper a OGNI dispatcher. A questo punto ho rinunciato perché non sembra fare di meglio per evitare il rifactoring globale. Il mio obiettivo è ridurre al minimo l'impatto sul chiamante quando il metodo di acquisizione dei dati viene modificato da sincrono ad asincrono nel livello DAO e Fiber non è ancora all'altezza della sfida.
abbr

@fred: non ha molto senso "sincronizzare" flussi di eventi come il gestore delle richieste - avresti bisogno di un while(true) handleNextRequest()ciclo. Avvolgere ogni gestore di richieste in una fibra.
Bergi

@fred: le fibre non ti aiuteranno molto con Express perché il callback di Express non è un callback di continuazione (un callback che viene sempre chiamato esattamente una volta, con un errore o con un risultato). Ma le fibre risolveranno la piramide del destino quando avrai un sacco di codice scritto sopra API asincrone con callback di continuazione (come fs, mongodb e molti altri).
Bruno Jouhier

-2

All'inizio ho lottato con questo con node.js e async.js è la migliore libreria che ho trovato per aiutarti a gestirlo. Se vuoi scrivere codice sincrono con node, l'approccio è in questo modo.

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();
  });
}

questo programma produrrà SEMPRE quanto segue ...

in main
step 1
step 2
step 3
done with things
back in main

2
asyncfunziona nel tuo esempio b / c è main, che non si preoccupa del chiamante. Immagina che tutto il tuo codice sia racchiuso in una funzione che dovrebbe restituire il risultato di una delle tue chiamate di funzione asincrone. Può essere facilmente verificato che non funziona aggiungendo console.log('return');alla fine del codice. In tal caso l'output di returnavverrà dopo in mainma prima step 1.
abbr

-11

Javascript è un linguaggio a thread singolo, non vuoi bloccare l'intero server! Il codice asincrono elimina le condizioni di competizione rendendo esplicite le dipendenze.

Impara ad amare il codice asincrono!

Dai un'occhiata al promisescodice asincrono senza creare una piramide dell'inferno di callback. Raccomando la libreria promiseQ per 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

EDIT: questa è di gran lunga la mia risposta più controversa, il nodo ora ha la parola chiave yield, che ti consente di trattare il codice asincrono come se fosse sincrono. http://blog.alexmaccaw.com/how-yield-will-transform-node


1
Promise riformula solo un parametro di callback invece di trasformare la funzione in sincronizzazione.
abbr

2
non vuoi che venga sincronizzato o l'intero server si bloccherà! stackoverflow.com/questions/17959663/...
roo2

1
Ciò che è desiderabile è una chiamata di sincronizzazione senza bloccare altri eventi come un'altra richiesta gestita da Node.js. Una funzione di sincronizzazione per definizione significa solo che non tornerà al chiamante fino a quando non verrà prodotto il risultato (non solo una promessa). Non esclude il server dalla gestione di altri eventi mentre la chiamata è bloccata.
abbr

@fred: penso che ti manchi il punto delle promesse . Non sono semplicemente un'astrazione del modello di osservazione, ma forniscono un modo per concatenare e comporre azioni asincrone.
Bergi

1
@Bergi, io uso promessa molto e so esattamente cosa fa. In effetti, tutto ciò che ha ottenuto è stato suddividere una singola chiamata di funzione asincrona in più invocazioni / istruzioni. Ma non cambia il risultato: quando il chiamante ritorna, non può restituire il risultato della funzione asincrona. Guarda l'esempio che ho pubblicato in JSFiddle. Il chiamante in questo caso è la funzione AnticipatedSyncFunction e la funzione asincrona è setTimeout. Se puoi rispondere alla mia sfida usando la promessa, per favore mostramelo.
abbr
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.