Koordinieren der parallelen Ausführung in node.js.

79

Das ereignisgesteuerte Programmiermodell von node.js macht es etwas schwierig, den Programmfluss zu koordinieren.

Einfache sequentielle Ausführung wird in verschachtelte Rückrufe umgewandelt, was einfach genug ist (obwohl es etwas kompliziert ist, es aufzuschreiben).

Aber wie wäre es mit paralleler Ausführung? Angenommen, Sie haben drei Aufgaben A, B, C, die parallel ausgeführt werden können, und wenn sie erledigt sind, möchten Sie ihre Ergebnisse an Aufgabe D senden.

Mit einem Fork / Join-Modell wäre dies

  • Gabel A.
  • Gabel B.
  • Gabel C.
  • Join A, B, C, Run D.

Wie schreibe ich das in node.js? Gibt es Best Practices oder Kochbücher? Muss ich jedes Mal eine Lösung von Hand rollen , oder gibt es dafür eine Bibliothek mit Helfern?

Thilo
quelle

Antworten:

128

In node.js ist nichts wirklich parallel, da es sich um Single-Threaded handelt. Es können jedoch mehrere Ereignisse geplant und in einer Reihenfolge ausgeführt werden, die Sie nicht im Voraus bestimmen können. Und einige Dinge wie der Datenbankzugriff sind tatsächlich "parallel", da die Datenbankabfragen selbst in separaten Threads ausgeführt werden, aber nach Abschluss wieder in den Ereignisstrom integriert werden.

Wie planen Sie einen Rückruf für mehrere Ereignishandler? Nun, dies ist eine gängige Technik, die in Animationen in browser-seitigem Javascript verwendet wird: Verwenden Sie eine Variable, um den Abschluss zu verfolgen.

Dies klingt wie ein Hack und ist es auch. Es klingt möglicherweise chaotisch und hinterlässt eine Reihe globaler Variablen, um das Tracking durchzuführen, und in einer geringeren Sprache wäre dies der Fall. Aber in Javascript können wir Verschlüsse verwenden:

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

Im obigen Beispiel halten wir den Code einfach, indem wir davon ausgehen, dass die Async- und Callback-Funktionen keine Argumente erfordern. Sie können den Code natürlich so ändern, dass Argumente an die asynchronen Funktionen übergeben werden und die Rückruffunktion Ergebnisse sammelt und an die Funktion shared_callback übergibt.


Zusätzliche Antwort:

Tatsächlich fork()kann diese Funktion , so wie sie ist, bereits Argumente über einen Abschluss an die asynchronen Funktionen übergeben:

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

Sie müssen nur noch die Ergebnisse von A, B, C akkumulieren und an D weitergeben.


Noch mehr zusätzliche Antwort:

Ich konnte nicht widerstehen. Ich habe beim Frühstück darüber nachgedacht. Hier ist eine Implementierung fork(), die Ergebnisse sammelt (normalerweise als Argumente an die Rückruffunktion übergeben):

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

Das war einfach genug. Dies ist ein fork()ziemlich allgemeiner Zweck und kann verwendet werden, um mehrere inhomogene Ereignisse zu synchronisieren.

Beispiel für die Verwendung in 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);

Aktualisieren

Dieser Code wurde vor der Existenz von Bibliotheken wie async.js oder den verschiedenen vielversprechenden Bibliotheken geschrieben. Ich würde gerne glauben, dass async.js davon inspiriert wurde, aber ich habe keinen Beweis dafür. Wie auch immer ... wenn Sie heute daran denken, werfen Sie einen Blick auf async.js oder Versprechen. Betrachten Sie die obige Antwort als eine gute Erklärung / Illustration dafür, wie Dinge wie async.parallel funktionieren.

Der Vollständigkeit halber würden Sie Folgendes tun async.parallel:

var async = require('async');

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

Beachten Sie, dass async.paralleldies genauso forkfunktioniert wie die oben implementierte Funktion. Der Hauptunterschied besteht darin, dass ein Fehler als erstes Argument an Dund der Rückruf als zweites Argument gemäß der Konvention node.js übergeben wird.

Mit Versprechungen würden wir es wie folgt schreiben:

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

Promise.all([A,B,C]).then(D);
Slebetman
quelle
12
"In node.js ist nichts wirklich parallel, da es sich um Single-Threaded handelt." Nicht wahr. Alles, was die CPU nicht verwendet (z. B. das Warten auf Netzwerk-E / A), läuft parallel.
Thilo
3
Es ist zum größten Teil wahr. Das Warten auf E / A in Node blockiert nicht die Ausführung von anderem Code, aber wenn der Code ausgeführt wird, ist es einer nach dem anderen. Die einzig wahre parallele Ausführung in Node besteht darin, untergeordnete Prozesse zu erzeugen, aber das kann man von fast jeder Umgebung sagen.
MooGoo
6
@Thilo: Normalerweise rufen wir Code auf, der die CPU nicht verwendet, da er nicht ausgeführt wird. Wenn Sie nicht laufen, können Sie nicht parallel "laufen".
Slebetman
4
@MooGoo: Dies impliziert, dass wir uns bei Ereignissen, da wir wissen, dass sie definitiv nicht parallel ausgeführt werden können, keine Gedanken über Semaphoren und Mutexe machen müssen, während wir bei Threads gemeinsam genutzte Ressourcen sperren müssen.
Slebetman
2
Habe ich Recht, wenn ich sage, dass dies keine Funktionen sind, die parallel ausgeführt werden, sondern (bestenfalls) in einer unbestimmten Reihenfolge ausgeführt werden, wobei der Code erst nach jeder Rückkehr von 'async_func' fortschreitet?
Aaron Rustad
10

Ich glaube, dass das "asynchrone" Modul jetzt diese parallele Funktionalität bietet und ungefähr der oben genannten Gabelfunktion entspricht.

Wes Gamble
quelle
2
Dies ist falsch. Mit Async können Sie Ihren Code-Fluss nur in einem einzigen Prozess organisieren.
Bwindels
2
async.parallel macht in der Tat ungefähr das Gleiche wie die obige forkFunktion
Dave Stibrany
Es ist keine echte Parallelität
Rab
5

Das Futures- Modul hat ein Submodul namens Join , das ich gerne verwendet habe:

Verbindet asynchrone Aufrufe ähnlich wie pthread_joinbei Threads.

Die Readme-Datei zeigt einige gute Beispiele für die Verwendung im Freestyle oder für die Verwendung des zukünftigen Submoduls mithilfe des Promise-Musters. Beispiel aus den Dokumenten:

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);
Geil
quelle
2

Hier könnte eine einfache Lösung möglich sein: http://howtonode.org/control-flow-part-ii Scrollen Sie zu Parallele Aktionen. Eine andere Möglichkeit wäre, A, B und C alle dieselbe Rückruffunktion zu haben, diese Funktion einen globalen oder zumindest funktionsunabhängigen Inkrementor zu haben. Wenn alle drei den Rückruf aufgerufen haben, lassen Sie ihn D ausführen. Natürlich müssen Sie die Ergebnisse von A, B und C auch irgendwo speichern.

Alex
quelle
0

Zusätzlich zu den beliebten Versprechungen und der Async-Bibliothek gibt es einen dritten eleganten Weg - mit "Verkabelung":

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

Daniel Garmoshka
quelle