Comment envelopper les appels de fonction asynchrone dans une fonction de synchronisation dans Node.js ou Javascript?


122

Supposons que vous mainteniez une bibliothèque qui expose une fonction getData. Vos utilisateurs l'appellent pour obtenir des données réelles:
var output = getData();
sous le capot, les données sont enregistrées dans un fichier, vous l'avez donc implémenté à l' getDataaide de Node.js intégré fs.readFileSync. C'est évident à la fois getDataet ce fs.readFileSyncsont des fonctions de synchronisation. Un jour, on vous a dit de basculer la source de données sous-jacente vers un dépôt tel que MongoDB qui ne peut être accédé que de manière asynchrone. On vous a également dit d'éviter d'énerver vos utilisateurs,getData API ne peut pas être modifiée pour renvoyer simplement une promesse ou exiger un paramètre de rappel. Comment répondez-vous aux deux exigences?

La fonction asynchrone utilisant le rappel / la promesse est l'ADN de JavasSript et Node.js. Toute application JS non triviale est probablement imprégnée de ce style de codage. Mais cette pratique peut facilement conduire à la soi-disant pyramide de callback of doom. Pire encore, si un code dans un appelant dans la chaîne d'appels dépend du résultat de la fonction asynchrone, ce code doit également être enveloppé dans la fonction de rappel, imposant une contrainte de style de codage à l'appelant. De temps en temps, je trouve le besoin d'encapsuler une fonction asynchrone (souvent fournie dans une bibliothèque tierce) dans une fonction de synchronisation afin d'éviter une refactorisation globale massive. La recherche d'une solution à ce sujet aboutissait généralement à des Node Fiberspackages ou npm dérivés. Mais Fibers ne peut tout simplement pas résoudre le problème auquel je suis confronté. Même l'exemple fourni par l'auteur de Fibers illustre la carence:

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

Sortie réelle:

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 fonction Fibre transforme réellement le sommeil de la fonction asynchrone en synchronisation, la sortie doit être:

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

J'ai créé un autre exemple simple dans JSFiddle et je cherche du code pour produire la sortie attendue. J'accepterai une solution qui ne fonctionne que dans Node.js, vous êtes donc libre d'exiger n'importe quel package npm même si vous ne travaillez pas dans JSFiddle.


2
Les fonctions async ne peuvent jamais être rendues synchrones dans Node, et même si elles le pouvaient, vous ne devriez pas. Le problème est tel que dans le module fs, vous pouvez voir des fonctions complètement séparées pour l'accès synchrone et asynchrone au système de fichiers. Le mieux que vous puissiez faire est de masquer l'apparence de l'async avec des promesses ou des coroutines (générateurs dans ES6). Pour gérer les pyramides de rappel, donnez-leur des noms au lieu de les définir dans un appel de fonction et utilisez quelque chose comme la bibliothèque async.
qubyte

8
Pour dandavis, async fait remonter les détails de l'implémentation dans la chaîne d'appels, forçant parfois le refactoring global. Ceci est préjudiciable et même désastreux pour une application complexe où la modularisation et le confinement sont importants.
abbr

4
"Callback pyramid of doom" n'est que la représentation du problème. Promise peut le cacher ou le déguiser mais ne peut pas relever le vrai défi: si l'appelant d'une fonction async dépend des résultats de la fonction async, il doit utiliser le rappel, tout comme son appelant, etc. Ceci est un exemple classique d'imposition de contraintes pour appelant simplement en raison des détails de mise en œuvre.
abbr

1
@abbr: Merci pour le module deasync, la description de votre problème est exactement ce que je cherchais, et je n'ai trouvé aucune solution viable. J'ai joué avec les générateurs et les itérables, mais je suis arrivé aux mêmes conclusions que vous.
Kevin Jhangiani

2
Il est à noter que ce n'est presque jamais une bonne idée de forcer une fonction async à se synchroniser. Vous avez presque toujours une meilleure solution qui maintient l'asynchronisme de la fonction intacte, tout en obtenant le même effet (comme le séquençage, le réglage des variables, etc.).
Madara's Ghost

Réponses:


104

deasync transforme la fonction async en synchronisation, implémentée avec un mécanisme de blocage en appelant la boucle d'événement Node.js au niveau de la couche JavaScript. En conséquence, deasync bloque uniquement l'exécution du code suivant sans bloquer tout le thread, ni entraîner une attente occupée. Avec ce module, voici la réponse au défi 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)

(avertissement: je suis le co-auteur de deasync. Le module a été créé après la publication de cette question et n'a trouvé aucune proposition viable.)


Quelqu'un d'autre a eu de la chance avec ça? Je ne peux pas le faire fonctionner.
newman

3
Je ne peux pas le faire fonctionner correctement. vous devriez améliorer votre documentation pour ce module, si vous souhaitez qu'il soit plus utilisé. Je doute que les auteurs sachent exactement quelles sont les ramifications de l'utilisation du module, et s'ils le font, ils ne les documentent certainement pas.
Alexander Mills

5
Jusqu'à présent, il existe un problème confirmé documenté dans le suivi des problèmes de github. Le problème a été résolu dans Node v0.12. Le reste que je connais ne sont que des spéculations sans fondement qui ne valent pas la peine d'être documentées. Si vous pensez que votre problème est causé par deasync, postez un scénario autonome et duplicable et je l'examinerai.
abbr

J'ai essayé de l'utiliser et j'obtiens quelques améliorations dans mon script mais je n'ai toujours pas eu de chance avec la date. J'ai modifié le code comme suit: 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); et je m'attends à voir 3 secondes de différence dans la sortie de la date!
Alex le

@abbr peut-il être navigué et utilisé sans dépendance de nœud>
Gandhi

5

Il existe également un module de synchronisation npm. qui est utilisé pour synchroniser le processus d'exécution de la requête.

Lorsque vous souhaitez exécuter des requêtes parallèles de manière synchrone, le nœud se limite à le faire car il n'attend jamais de réponse. et le module de synchronisation est parfait pour ce type de solution.

Exemple de code

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

lien de référence: https://www.npmjs.com/package/sync


4

Si la fonction Fibre transforme vraiment le sommeil de la fonction asynchrone en synchronisation

Oui. À l'intérieur de la fibre, la fonction attend avant de se connecter ok. Les fibres ne rendent pas les fonctions asynchrones synchrones, mais permettent d'écrire du code d'apparence synchrone qui utilise des fonctions asynchrones, puis s'exécutera de manière asynchrone dans un fichier Fiber.

De temps en temps, je trouve le besoin d'encapsuler une fonction asynchrone dans une fonction de synchronisation afin d'éviter une refactorisation globale massive.

Vous ne pouvez pas. Il est impossible de rendre le code asynchrone synchrone. Vous devrez anticiper cela dans votre code global et l'écrire dans un style asynchrone depuis le début. Que vous enveloppiez le code global dans une fibre, utilisiez des promesses, des générateurs de promesses ou de simples rappels dépend de vos préférences.

Mon objectif est de minimiser l'impact sur l'appelant lorsque la méthode d'acquisition de données passe de sync à async

Les promesses et les fibres peuvent le faire.


1
c'est la pire chose ABSOLUE que vous puissiez faire avec Node.js: "du code à l'aspect synchrone qui utilise des fonctions asynchrones et qui s'exécutera ensuite de manière asynchrone." si votre API fait cela, vous ruinerez des vies. s'il est asynchrone, il devrait nécessiter un rappel et renvoyer une erreur si aucun rappel n'est fourni. c'est la meilleure façon de créer une API, sauf si votre objectif est de tromper les gens.
Alexander Mills

@AlexMills: Oui, ce serait vraiment horrible . Cependant, heureusement, ce n'est rien qu'une API peut faire. Une API asynchrone doit toujours accepter un rappel / renvoyer une promesse / s'attendre à être exécutée à l'intérieur d'une fibre - cela ne fonctionne pas sans. Afaik, les fibres étaient principalement utilisées dans les scripts quick'n'dirty qui bloquaient et n'avaient pas de concurrence, mais qui souhaitaient utiliser des API asynchrones; tout comme dans node, il y a parfois des cas où vous utiliseriez les fsméthodes synchrones .
Bergi

2
J'aime généralement node. Surtout si je peux utiliser dactylographié au lieu de pur js. Mais tout ce non-sens asynchrone qui imprègne tout ce que vous faites et infecte littéralement toutes les fonctions de la chaîne d'appels dès que vous décidez de faire un seul appel asynchrone est quelque chose que je ... vraiment déteste. Async api est comme une maladie infectieuse, un seul appel infecte toute votre base de code, vous obligeant à réécrire tout le code que vous avez. Je ne comprends vraiment pas comment quiconque peut prétendre que c'est une bonne chose.
Kris

@Kris Node utilise un modèle asynchrone pour les tâches d'E / S car il est rapide et simple. Vous pouvez également faire beaucoup de choses de manière synchrone, mais le blocage est lent car vous ne pouvez rien faire simultanément - sauf si vous optez pour les threads, ce qui complique tout.
Bergi

@Bergi J'ai lu le manifeste donc je connais les arguments. Mais changer votre code existant en asynchrone au moment où vous frappez ce premier appel d'API qui n'a pas d'équivalent de synchronisation n'est pas simple. Tout se brise et chaque ligne de code doit être examinée. À moins que votre code ne soit trivial, je vous le garantis ... il faudra un certain temps pour le convertir et le faire fonctionner à nouveau après la conversion du tout en idiome asynchrone.
Kris

2

Vous devez utiliser les promesses:

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

J'aime plus les définitions des fonctions fléchées. Mais toute chaîne de la forme "() => {...}" pourrait aussi être écrite comme "function () {...}"

Ainsi, topDog n'est pas asynchrone malgré l'appel d'une fonction async.

entrez la description de l'image ici

EDIT: Je me rends compte que la plupart du temps, vous devez envelopper une fonction asynchrone dans une fonction de synchronisation à l'intérieur d'un contrôleur. Pour ces situations, voici une astuce de fête:

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

En utilisant cela avec des rappels, vous pouvez faire un wrap qui n'utilise pas de promesses:

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

En appliquant cette astuce à un EventEmitter, vous pouvez obtenir les mêmes résultats. Définissez l'écouteur de l'EventEmitter où j'ai défini le rappel et émettez l'événement où j'ai appelé le rappel.


1

Je ne trouve pas de scénario qui ne puisse pas être résolu en utilisant des fibres de nœuds. L'exemple que vous avez fourni à l'aide de nœuds-fibres se comporte comme prévu. La clé est d'exécuter tout le code pertinent à l'intérieur d'une fibre, afin que vous n'ayez pas à démarrer une nouvelle fibre dans des positions aléatoires.

Voyons un exemple: disons que vous utilisez un framework, qui est le point d'entrée de votre application (vous ne pouvez pas modifier ce framework). Ce framework charge les modules nodejs en tant que plugins et appelle certaines méthodes sur les plugins. Disons que ce cadre n'accepte que les fonctions synchrones et n'utilise pas de fibres par lui-même.

Il existe une bibliothèque que vous souhaitez utiliser dans l'un de vos plugins, mais cette bibliothèque est asynchrone et vous ne souhaitez pas non plus la modifier.

Le thread principal ne peut pas être généré lorsqu'aucune fibre n'est en cours d'exécution, mais vous pouvez toujours créer des plugins en utilisant des fibres! Créez simplement une entrée wrapper qui démarre tout le framework à l'intérieur d'une fibre, afin de pouvoir générer l'exécution à partir des plugins.

Inconvénient: si le framework utilise setTimeoutou Promises en interne, il échappera au contexte de la fibre. Cela peut être contourné en se moquant setTimeout, Promise.thenet tous les gestionnaires d'événements.

C'est ainsi que vous pouvez produire une fibre jusqu'à ce que a Promisesoit résolu. Ce code prend une fonction asynchrone (Promise retournant) et reprend la fibre lorsque la promesse est résolue:

framework-entry.js

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

async-lib.js

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

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

mon-entrée.js

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

Lorsque vous exécutez node framework-entry.jsil lancera une erreur: Error: yield() called with no fiber running. Si vous exécutez node my-entry.jscela fonctionne comme prévu.


0

La synchronisation du code Node.js est essentielle dans certains aspects tels que la base de données. Mais l'avantage réel de Node.js réside dans le code asynchrone. Comme il s'agit d'un seul thread non bloquant.

nous pouvons le synchroniser en utilisant une fonctionnalité importante Fiber () Utilisez await () et defer () nous appelons toutes les méthodes en utilisant await (). puis remplacez les fonctions de rappel par defer ().

Code Async normal, qui utilise les fonctions de rappel.

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

Synchronisez le code ci-dessus en utilisant Fiber (), await () et 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()));

});

J'espère que cela aidera. Merci


0

De nos jours, ce modèle de générateur peut être une solution dans de nombreuses situations.

Voici un exemple d'invites de console séquentielles dans nodejs à l'aide de la fonction 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

-1

Vous ne devriez pas regarder ce qui se passe autour de l'appel qui crée la fibre, mais plutôt ce qui se passe à l' intérieur de la fibre. Une fois que vous êtes à l'intérieur de la fibre, vous pouvez programmer dans le style de synchronisation. Par exemple:

fonction f1 () {
    console.log ('attendre ...' + nouvelle date);
    sommeil (1000);
    console.log ('ok ...' + nouvelle date);   
}

fonction f2 () {
    f1 ();
    f1 ();
}

Fibre (fonction () {
    f2 ();
}).courir();

À l'intérieur de la fibre que vous appelez f1, f2et sleepcomme s'ils étaient synchronisés.

Dans une application Web classique, vous allez créer la fibre dans votre répartiteur de requêtes HTTP. Une fois que vous avez fait cela, vous pouvez écrire toute votre logique de gestion des demandes dans le style de synchronisation, même si elle appelle des fonctions asynchrones (fs, bases de données, etc.).


Merci Bruno. Mais que se passe-t-il si j'ai besoin d'un style de synchronisation dans le code d'amorçage qui doit être exécuté avant que le serveur ne se lie au port TCP - comme la configuration ou les données qui doivent être lues à partir de la base de données ouverte de manière asynchrone? J'ai peut-être fini par envelopper tout le serveur.js dans Fiber, et je soupçonne que cela éliminera la concurrence à tout le niveau du processus. Néanmoins, c'est une suggestion qui mérite d'être vérifiée. Pour moi, la solution idéale devrait être capable d'encapsuler une fonction asynchrone pour fournir une syntaxe d'appel de synchronisation et ne bloque que les prochaines lignes de code dans la chaîne d'appel sans sacrifier la concurrence au niveau du processus.
abbr

Vous pouvez envelopper l'intégralité de votre code bootstrap dans un seul gros appel Fibre. La concurrence ne devrait pas être un problème car le code d'amorçage doit généralement s'exécuter jusqu'à la fin avant de commencer à traiter les demandes. De plus, une fibre n'empêche pas les autres fibres de couler: chaque fois que vous frappez un appel de rendement, vous donnez aux autres fibres (et au fil principal) une chance de courir.
Bruno Jouhier

J'ai enveloppé le fichier de démarrage express server.js avec de la fibre. La séquence d'exécution est ce que je recherche, mais cette enveloppe n'a aucun effet sur le gestionnaire de requêtes. Je suppose donc que je dois appliquer le même wrapper à CHAQUE répartiteur. J'ai abandonné à ce stade parce que cela ne semble pas faire mieux pour éviter une refactorisation globale. Mon objectif est de minimiser l'impact sur l'appelant lorsque la méthode d'acquisition de données passe de la synchronisation à l'asynchrone dans la couche DAO et que la fibre est toujours un peu en deçà du défi.
abbr

@fred: Cela n'a pas beaucoup de sens de "synchroniser" les flux d'événements comme le gestionnaire de requêtes - vous auriez besoin d'une while(true) handleNextRequest()boucle. Emballer chaque gestionnaire de demande dans une fibre le ferait.
Bergi

@fred: les fibres ne vous aideront pas beaucoup avec Express car le callback d'Express n'est pas un callback de continuation (un callback qui est toujours appelé exactement une fois, soit avec une erreur, soit avec un résultat). Mais les fibres résoudront la pyramide du malheur lorsque vous aurez beaucoup de code écrit au-dessus des API asynchrones avec des rappels de continuation (comme fs, mongodb et bien d'autres).
Bruno Jouhier

-2

J'ai eu du mal avec cela au début avec node.js et async.js est la meilleure bibliothèque que j'ai trouvée pour vous aider à gérer cela. Si vous souhaitez écrire du code synchrone avec node, l'approche est la suivante.

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

ce programme produira TOUJOURS ce qui suit ...

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

2
asyncfonctionne dans votre exemple b / c c'est main, qui ne se soucie pas de l'appelant. Imaginez que tout votre code est enveloppé dans une fonction qui est censée renvoyer le résultat de l'un de vos appels de fonction asynchrone. Il peut être facilement prouvé qu'il ne fonctionne pas en ajoutant console.log('return');à la fin de votre code. Dans ce cas, la sortie de returnse produira après in mainmais avant step 1.
abbr

-11

Javascript est un langage à thread unique, vous ne voulez pas bloquer tout votre serveur! Le code async élimine les conditions de concurrence en rendant les dépendances explicites.

Apprenez à aimer le code asynchrone!

Jetez un œil au promisescode asynchrone sans créer une pyramide de l'enfer des rappels. Je recommande la bibliothèque promiseQ pour 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: c'est de loin ma réponse la plus controversée, node a maintenant le mot-clé yield, qui vous permet de traiter le code asynchrone comme s'il était synchrone. http://blog.alexmaccaw.com/how-yield-will-transform-node


1
Promise ne fait que reformuler un paramètre de rappel plutôt que de synchroniser la fonction.
abbr

2
vous ne voulez pas qu'il soit synchronisé ou tout votre serveur se bloquera! stackoverflow.com/questions/17959663/…
roo2

1
Ce qui est souhaitable est un appel de synchronisation sans bloquer d'autres événements tels qu'une autre requête gérée par Node.js. Une fonction Sync par définition signifie seulement qu'elle ne reviendra pas à l'appelant tant que le résultat ne sera pas produit (pas simplement une promesse). Cela n'empêche pas le serveur de gérer d'autres événements pendant que l'appel est bloqué.
abbr

@fred: Je pense que vous manquez le point des promesses . Ils ne sont pas simplement une abstraction de modèle d'observateur, mais ils fournissent un moyen d'enchaîner et de composer des actions asynchrones.
Bergi

1
@Bergi, j'utilise beaucoup la promesse et je sais exactement ce qu'elle fait. En fait, tout ce qu'il a réalisé est de décomposer une seule invocation de fonction asynchrone en plusieurs invocations / instructions. Mais cela ne change pas le résultat - lorsque l'appelant revient, il ne peut pas renvoyer le résultat de la fonction asynchrone. Consultez l'exemple que j'ai publié dans JSFiddle. L'appelant dans ce cas est la fonction AnticipatedSyncFunction et la fonction async est setTimeout. Si vous pouvez répondre à mon défi en utilisant la promesse, veuillez me montrer.
abbr
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.