web-dev-qa-db-fra.com

Coordonner l'exécution parallèle dans node.js

Le modèle de programmation événementiel de node.js rend difficile la coordination du déroulement du programme.

Une exécution séquentielle simple est transformée en rappels imbriqués, ce qui est assez facile (bien qu'un peu compliqué à écrire). 

Mais qu'en est-il de l'exécution parallèle? Supposons que vous ayez trois tâches A, B et C pouvant être exécutées en parallèle et que, lorsqu'elles sont terminées, vous souhaitez envoyer leurs résultats à la tâche D.

Avec un modèle fork/join, ce serait

  • fourchette A
  • fourchette B
  • fourchette C
  • rejoindre A, B, C, exécuter D

Comment puis-je écrire cela dans node.js? Existe-t-il des meilleures pratiques ou des livres de cuisine? Dois-je proposer à la main une solution à chaque fois, ou existe-t-il une bibliothèque avec des aides pour cela?

79
Thilo

Rien n'est vraiment parallèle dans node.js puisqu'il s'agit d'un seul thread. Cependant, plusieurs événements peuvent être planifiés et exécutés dans un ordre que vous ne pouvez pas déterminer à l'avance. Et certaines choses comme l'accès à la base de données sont en réalité "parallèles" en ce sens que les requêtes de base de données sont exécutées dans des threads distincts mais sont réintégrées dans le flux d'événements une fois l'opération terminée.

Alors, comment planifiez-vous un rappel sur plusieurs gestionnaires d'événements? Eh bien, il s’agit d’une technique courante utilisée dans les animations dans le javascript côté navigateur: utilisez une variable pour suivre l’achèvement.

Cela ressemble à un bidouillage et ça l'est, et cela peut sembler désordonné, laissant un tas de variables globales autour du suivi et dans un langage moins strict, ce serait. Mais en javascript, nous pouvons utiliser des fermetures:

function fork (async_calls, shared_callback) {
  var counter = async_calls.length;
  var callback = function () {
    counter --;
    if (counter == 0) {
      shared_callback()
    }
  }

  for (var i=0;i<async_calls.length;i++) {
    async_calls[i](callback);
  }
}

// usage:
fork([A,B,C],D);

Dans l'exemple ci-dessus, le code reste simple en supposant que les fonctions async et callback ne nécessitent aucun argument. Vous pouvez bien sûr modifier le code pour passer des arguments aux fonctions asynchrones et faire en sorte que la fonction de rappel accumule les résultats et les transmette à la fonction shared_callback.


Réponse supplémentaire:

En réalité, même telle quelle, cette fonction fork() peut déjà transmettre des arguments aux fonctions asynchrones à l'aide d'une fermeture:

fork([
  function(callback){ A(1,2,callback) },
  function(callback){ B(1,callback) },
  function(callback){ C(1,2,callback) }
],D);

il ne reste plus qu'à accumuler les résultats de A, B, C et à les transmettre à D.


Encore plus de réponse supplémentaire:

Je n'ai pas pu résister. Je pensais à cela pendant le petit déjeuner. Voici une implémentation de fork() qui accumule les résultats (généralement passés en tant qu'arguments à la fonction de rappel):

function fork (async_calls, shared_callback) {
  var counter = async_calls.length;
  var all_results = [];
  function makeCallback (index) {
    return function () {
      counter --;
      var results = [];
      // we use the arguments object here because some callbacks 
      // in Node pass in multiple arguments as result.
      for (var i=0;i<arguments.length;i++) {
        results.Push(arguments[i]);
      }
      all_results[index] = results;
      if (counter == 0) {
        shared_callback(all_results);
      }
    }
  }

  for (var i=0;i<async_calls.length;i++) {
    async_calls[i](makeCallback(i));
  }
}

C'était assez facile. Cela rend fork() plutôt général et peut être utilisé pour synchroniser plusieurs événements non homogènes.

Exemple d'utilisation dans Node.js:

// Read 3 files in parallel and process them together:

function A (c){ fs.readFile('file1',c) };
function B (c){ fs.readFile('file2',c) };
function C (c){ fs.readFile('file3',c) };
function D (result) {
  file1data = result[0][1];
  file2data = result[1][1];
  file3data = result[2][1];

  // process the files together here
}

fork([A,B,C],D);

Mettre à jour

Ce code a été écrit avant l'existence de bibliothèques telles que async.js ou des diverses bibliothèques basées sur des promesses. J'aimerais croire que async.js a été inspiré par cela, mais je n'en ai aucune preuve. Quoi qu'il en soit .. si vous envisagez de faire cela aujourd'hui, jetez un coup d'œil à async.js ou à vos promesses. Considérez simplement la réponse ci-dessus comme une bonne explication/illustration du fonctionnement de choses comme async.parallel.

Par souci d’exhaustivité, voici comment procéder avec async.parallel:

var async = require('async');

async.parallel([A,B,C],D);

Notez que async.parallel fonctionne exactement de la même manière que la fonction fork que nous avons implémentée ci-dessus. La principale différence est qu’il passe une erreur en tant que premier argument à D et le rappel en tant que deuxième argument, conformément à la convention node.js.

En utilisant des promesses, nous l’écririons comme suit:

// Assuming A, B & C return a promise instead of accepting a callback

Promise.all([A,B,C]).then(D);
125
slebetman

Je crois que maintenant le module "async" fournit cette fonctionnalité parallèle et est à peu près identique à la fonction fork précédemment.

10
Wes Gamble

Le module futures a un sous-module appelé join que j’ai aimé utiliser:

Joint les appels asynchrones de la même manière que pthread_join pour les threads.

Le fichier Lisez-moi montre quelques bons exemples d'utilisation de ce style libre ou du sous-module future en utilisant le modèle Promise. Exemple tiré de la documentation:

var Join = require('join')
  , join = Join()
  , callbackA = join.add()
  , callbackB = join.add()
  , callbackC = join.add();

function abcComplete(aArgs, bArgs, cArgs) {
  console.log(aArgs[1] + bArgs[1] + cArgs[1]);
}

setTimeout(function () {
  callbackA(null, 'Hello');
}, 300);

setTimeout(function () {
  callbackB(null, 'World');
}, 500);

setTimeout(function () {
  callbackC(null, '!');
}, 400);

// this must be called after all 
join.when(abcComplete);
5
Randy

Une autre option pourrait être le module Step pour Node: https://github.com/creationix/step

2
Wilhelm Murdoch

Une solution simple pourrait être possible ici: http://howtonode.org/control-flow-part-ii scroll to Parallel actions. Une autre solution serait de faire en sorte que A, B et C partagent tous la même fonction de rappel, que cette fonction ait un incrémenteur global ou au moins hors fonction, si tous les trois ont appelé le rappel, puis le laisser exécuter D, Bien sûr, vous devrez également stocker les résultats de A, B et C quelque part.

2
Alex

Vous pouvez essayer cette petite bibliothèque: https://www.npmjs.com/package/parallel-io

0
Konstantin

En plus des promesses populaires et de la bibliothèque asynchrone, il existe une 3ème manière élégante - utilisant le "câblage":

var l = new Wire();

funcA(l.branch('post'));
funcB(l.branch('comments'));
funcC(l.branch('links'));

l.success(function(results) {
   // result will be object with results:
   // { post: ..., comments: ..., links: ...}
});

https://github.com/garmoshka-mo/mo-wire

0
Dan Key