Manière idiomatique d’attendre plusieurs rappels dans Node.js

Supposons que vous deviez faire des opérations qui dépendent de certains fichiers temporaires. Puisque nous parlons de Node ici, ces opérations sont évidemment asynchrones. Quel est le moyen idiomatique d’attendre que toutes les opérations se terminent pour savoir quand le fichier temporaire peut être supprimé?

Voici un code indiquant ce que je veux faire:

do_something(tmp_file_name, function(err) {}); do_something_other(tmp_file_name, function(err) {}); fs.unlink(tmp_file_name); 

Mais si je l’écris de cette façon, le troisième appel peut être exécuté avant que les deux premiers aient la possibilité d’utiliser le fichier. J’ai besoin d’un moyen de garantir que les deux premiers appels sont déjà terminés (invoqués par leurs rappels) avant de continuer sans imbriquer les appels (et de les rendre synchrones dans la pratique).

J’ai pensé à utiliser des émetteurs d’événements sur les rappels et à enregistrer un compteur en tant que récepteur. Le compteur recevrait les événements terminés et comptait le nombre d’opérations en attente. Une fois la dernière terminée, le fichier serait supprimé. Mais il y a le risque d’une situation de course et je ne suis pas sûr que ce soit généralement comme ça que ça se passe.

Comment les personnes Node résolvent-elles ce genre de problème?

Mettre à jour:

Maintenant, je conseillerais de regarder:

  • Promesses

    L’object Promise est utilisé pour les calculs différés et asynchrones. Une promesse représente une opération qui n’est pas encore terminée, mais qui est attendue dans le futur.

    Une bibliothèque de promesses populaires est bluebird . A conseille de jeter un coup d’oeil aux raisons des promesses .

    Vous devriez utiliser les promesses pour transformer ceci:

     fs.readFile("file.json", function (err, val) { if (err) { console.error("unable to read file"); } else { try { val = JSON.parse(val); console.log(val.success); } catch (e) { console.error("invalid json in file"); } } }); 

    Dans ceci:

     fs.readFileAsync("file.json").then(JSON.parse).then(function (val) { console.log(val.success); }) .catch(SyntaxError, function (e) { console.error("invalid json in file"); }) .catch(function (e) { console.error("unable to read file"); }); 
  • générateurs: Par exemple via co .

    Contrôle du stream basé sur les générateurs pour nodejs et le navigateur, en utilisant des promesses, vous permettant d’écrire du code non bloquant d’une manière agréable.

     var co = require('co'); co(function *(){ // yield any promise var result = yield Promise.resolve(true); }).catch(onerror); co(function *(){ // resolve multiple promises in parallel var a = Promise.resolve(1); var b = Promise.resolve(2); var c = Promise.resolve(3); var res = yield [a, b, c]; console.log(res); // => [1, 2, 3] }).catch(onerror); // errors can be try/catched co(function *(){ try { yield Promise.reject(new Error('boom')); } catch (err) { console.error(err.message); // "boom" } }).catch(onerror); function onerror(err) { // log any uncaught errors // co will not throw any errors you do not handle!!! // HANDLE ALL YOUR ERRORS!!! console.error(err.stack); } 

Si je comprends bien, je pense que vous devriez jeter un oeil à la très bonne bibliothèque asynchrone . Vous devriez surtout regarder la série . Juste une copie des extraits de la page github:

 async.series([ function(callback){ // do some stuff ... callback(null, 'one'); }, function(callback){ // do some more stuff ... callback(null, 'two'); }, ], // optional callback function(err, results){ // results is now equal to ['one', 'two'] }); // an example using an object instead of an array async.series({ one: function(callback){ setTimeout(function(){ callback(null, 1); }, 200); }, two: function(callback){ setTimeout(function(){ callback(null, 2); }, 100); }, }, function(err, results) { // results is now equals to: {one: 1, two: 2} }); 

De plus, cette bibliothèque peut également être exécutée dans le navigateur.

La méthode la plus simple consiste à incrémenter un compteur entier lorsque vous lancez une opération asynchrone puis, dans le rappel, décrémentez le compteur. Selon la complexité, le rappel peut vérifier le compteur pour zéro, puis supprimer le fichier.

Un peu plus complexe serait de maintenir une liste d’objects, et chaque object aurait des atsortingbuts dont vous avez besoin pour identifier l’opération (cela pourrait même être l’appel de la fonction) ainsi qu’un code d’état. Les rappels définiraient le code d’état à terminé.

Ensuite, vous aurez une boucle qui attend (en utilisant process.nextTick ) et vérifie si toutes les tâches sont terminées. L’avantage de cette méthode par rapport au compteur est que si toutes les tâches en attente sont terminées, avant que toutes les tâches ne soient exécutées, la technique du compteur vous obligerait à supprimer le fichier prématurément.

 // simple countdown latch function CDL(countdown, completion) { this.signal = function() { if(--countdown < 1) completion(); }; } // usage var latch = new CDL(10, function() { console.log("latch.signal() was called 10 times."); }); 

Il n’y a pas de solution “native”, mais il y a un million de bibliothèques de contrôle de stream pour node. Vous pourriez aimer Step:

 Step( function(){ do_something(tmp_file_name, this.parallel()); do_something_else(tmp_file_name, this.parallel()); }, function(err) { if (err) throw err; fs.unlink(tmp_file_name); } ) 

Ou, comme Michael l’a suggéré, les compteurs pourraient être une solution plus simple. Jetez un oeil à cette maquette de sémaphore . Vous l’utiliseriez comme ceci:

 do_something1(file, queue('myqueue')); do_something2(file, queue('myqueue')); queue.done('myqueue', function(){ fs.unlink(file); }); 

J’aimerais proposer une autre solution qui utilise la rapidité et l’efficacité du paradigme de programmation au cœur même de Node: events.

Tout ce que vous pouvez faire avec Promises ou des modules conçus pour gérer le contrôle de stream, comme l’ async , peut être accompli en utilisant des événements et une simple machine à états, qui offre une méthodologie peut-être plus facile à comprendre que d’autres options.

Par exemple, supposons que vous souhaitiez faire la sum de plusieurs fichiers en parallèle:

 const EventEmitter = require('events').EventEmitter; // simple event-driven state machine const sm = new EventEmitter(); // running state let context={ tasks: 0, // number of total tasks active: 0, // number of active tasks results: [] // task results }; const next = (result) => { // must be called when each task chain completes if(result) { // preserve result of task chain context.results.push(result); } // decrement the number of running tasks context.active -= 1; // when all tasks complete, sortinggger done state if(!context.active) { sm.emit('done'); } }; // operational states // start state - initializes context sm.on('start', (paths) => { const len=paths.length; console.log(`start: beginning processing of ${len} paths`); context.tasks = len; // total number of tasks context.active = len; // number of active tasks sm.emit('forEachPath', paths); // go to next state }); // start processing of each path sm.on('forEachPath', (paths)=>{ console.log(`forEachPath: starting ${paths.length} process chains`); paths.forEach((path) => sm.emit('readPath', path)); }); // read contents from path sm.on('readPath', (path) => { console.log(` readPath: ${path}`); fs.readFile(path,(err,buf) => { if(err) { sm.emit('error',err); return; } sm.emit('processContent', buf.toSsortingng(), path); }); }); // compute length of path contents sm.on('processContent', (str, path) => { console.log(` processContent: ${path}`); next(str.length); }); // when processing is complete sm.on('done', () => { const total = context.results.reduce((sum,n) => sum + n); console.log(`The total of ${context.tasks} files is ${total}`); }); // error state sm.on('error', (err) => { throw err; }); // ====================================================== // start processing - ok, let's go // ====================================================== sm.emit('start', ['file1','file2','file3','file4']); 

Qui va sortir:

 début: début du traitement de 4 chemins
 forEachPath: démarrage de 4 chaînes de processus
   readPath: fichier1
   readPath: fichier2
   processContent: fichier1
   readPath: fichier3
   processContent: fichier2
   processContent: fichier3
   readPath: fichier4
   processContent: fichier4
 Le total de 4 fichiers est de 4021

Notez que l’ordre des tâches de chaîne de processus dépend de la charge du système.

Vous pouvez envisager le déroulement du programme comme suit:

 start -> forEachPath - + -> readPath 1 -> processContent 1 - + -> done
                       + -> readFile 2 -> processContent 2 - +
                       + -> readFile 3 -> processContent 3 - +
                       + -> readFile 4 -> processContent 4 - +

Pour la réutilisation, il serait sortingvial de créer un module pour prendre en charge les différents modèles de contrôle de stream, c.-à-d. Série, parallèle, batch, tandis que, jusqu’à, etc.

La solution la plus simple consiste à exécuter do_something * et à dissocier les séquences comme suit:

 do_something(tmp_file_name, function(err) { do_something_other(tmp_file_name, function(err) { fs.unlink(tmp_file_name); }); }); 

À moins que, pour des raisons de performances, vous ne vouliez exécuter do_something () et do_something_other () en parallèle, je suggère de restr simple et de procéder de cette manière.

Attendez pour https://github.com/luciotato/waitfor

en utilisant Wait.for:

 var wait=require('wait.for'); ...in a fiber... wait.for(do_something,tmp_file_name); wait.for(do_something_other,tmp_file_name); fs.unlink(tmp_file_name);