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
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?
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.
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.
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);
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);
Je crois que maintenant le module "async" fournit cette fonctionnalité parallèle et est à peu près identique à la fonction fork précédemment.
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);
Une autre option pourrait être le module Step pour Node: https://github.com/creationix/step
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.
Vous pouvez essayer cette petite bibliothèque: https://www.npmjs.com/package/parallel-io
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: ...}
});