Koordynowanie równoległego wykonywania w node.js

79

Model programowania oparty na zdarzeniach node.js sprawia, że ​​koordynacja przepływu programu jest nieco trudna.

Proste sekwencyjne wykonywanie zostaje zamienione w zagnieżdżone wywołania zwrotne, co jest dość łatwe (choć zapisywanie jest nieco zawiłe).

Ale co z równoległym wykonaniem? Załóżmy, że masz trzy zadania A, B, C, które mogą być wykonywane równolegle, a kiedy są wykonane, chcesz wysłać ich wyniki do zadania D.

Z modelem rozwidlenia / złączenia byłoby to

  • widelec A
  • widelec B
  • widelec C
  • dołącz do A, B, C, uruchom D

Jak to napisać w node.js? Czy są jakieś najlepsze praktyki lub książki kucharskie? Czy za każdym razem muszę ręcznie wprowadzać rozwiązanie , czy też jest jakaś biblioteka z pomocnikami?

Thilo
źródło

Odpowiedzi:

128

Nic nie jest naprawdę równoległe w node.js, ponieważ jest on jednowątkowy. Jednak wiele wydarzeń można zaplanować i uruchomić w kolejności, której nie można wcześniej określić. Niektóre rzeczy, takie jak dostęp do bazy danych, są w rzeczywistości „równoległe”, ponieważ same zapytania do bazy danych są uruchamiane w oddzielnych wątkach, ale po zakończeniu są ponownie integrowane ze strumieniem zdarzeń.

Jak więc zaplanować wywołanie zwrotne w wielu programach obsługi zdarzeń? Cóż, jest to jedna z powszechnych technik używanych w animacjach w javascript po stronie przeglądarki: użyj zmiennej do śledzenia ukończenia.

Brzmi to jak hack i tak właśnie jest, i brzmi potencjalnie niechlujnie, pozostawiając kilka zmiennych globalnych wokół wykonywania śledzenia i w mniejszym języku. Ale w javascript możemy użyć domknięć:

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

W powyższym przykładzie utrzymujemy kod w prostocie, zakładając, że funkcje asynchroniczne i wywołania zwrotne nie wymagają argumentów. Możesz oczywiście zmodyfikować kod tak, aby przekazywał argumenty do funkcji asynchronicznych, a funkcja wywołania zwrotnego gromadzi wyniki i przekazuje je do funkcji shared_callback.


Dodatkowa odpowiedź:

Właściwie, nawet jeśli jest, ta fork()funkcja może już przekazywać argumenty do funkcji asynchronicznych za pomocą zamknięcia:

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

pozostaje tylko zebrać wyniki z punktów A, B, C i przekazać je do D.


Jeszcze bardziej dodatkowa odpowiedź:

Nie mogłem się oprzeć. Myślałem o tym podczas śniadania. Oto implementacja, fork()która gromadzi wyniki (zwykle przekazywane jako argumenty do funkcji zwrotnej):

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

To było dość łatwe. Ma to fork()dość ogólny cel i może być używane do synchronizowania wielu niejednorodnych zdarzeń.

Przykładowe użycie w 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);

Aktualizacja

Ten kod został napisany przed istnieniem bibliotek, takich jak async.js lub różne biblioteki oparte na obietnicach. Chciałbym wierzyć, że async.js został zainspirowany tym, ale nie mam na to żadnego dowodu. W każdym razie ... jeśli myślisz o zrobieniu tego dzisiaj, spójrz na async.js lub obietnice. Po prostu rozważ powyższą odpowiedź jako dobre wyjaśnienie / ilustrację tego, jak działają rzeczy takie jak asynchroniczne równoległe.

Ze względu na kompletność poniżej przedstawiono, jak to zrobić z async.parallel:

var async = require('async');

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

Zauważ, że async.paralleldziała dokładnie tak samo, jak forkfunkcja, którą zaimplementowaliśmy powyżej. Główna różnica polega na tym, że przekazuje błąd jako pierwszy argument, Da wywołanie zwrotne jako drugi argument zgodnie z konwencją node.js.

Korzystając z obietnic, napisalibyśmy to w następujący sposób:

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

Promise.all([A,B,C]).then(D);
slebetman
źródło
12
„Nic nie jest naprawdę równoległe w node.js, ponieważ jest jednowątkowy”. Nie prawda. Wszystko, co nie korzysta z procesora (np. Oczekiwanie na sieciowe I / O), działa równolegle.
Thilo,
3
W większości to prawda. Oczekiwanie na IO w Node nie blokuje uruchomienia innego kodu, ale gdy kod jest uruchamiany, jest wykonywany pojedynczo. Jedynym prawdziwym równoległym wykonaniem w Node jest tworzenie procesów potomnych, ale można to powiedzieć o prawie każdym środowisku.
MooGoo
6
@Thilo: Zwykle nazywamy kod, który nie używa procesora, jako niedziałający. Jeśli nie bierzesz, nie możesz „biegać” równolegle.
slebetman
4
@MooGoo: Konsekwencją tego jest to, że w przypadku zdarzeń, ponieważ wiemy, że zdecydowanie nie mogą działać równolegle, nie musimy martwić się o semafory i muteksy, podczas gdy w przypadku wątków musimy blokować współdzielone zasoby.
slebetman
2
Czy mam rację mówiąc, że nie są to funkcje wykonywane równolegle, ale (w najlepszym przypadku) wykonywane w nieokreślonej sekwencji, a kod nie postępuje, dopóki nie powróci każdy „async_func”?
Aaron Rustad
10

Uważam, że teraz moduł „async” zapewnia tę równoległą funkcjonalność i jest mniej więcej taki sam, jak powyższa funkcja fork.

Wes Gamble
źródło
2
To jest niepoprawne, asynchronizacja pomaga tylko zorganizować przepływ kodu w ramach jednego procesu.
bwindels
2
async.parallel faktycznie robi mniej więcej to samo, co powyższa forkfunkcja
Dave Stibrany
to nie jest prawdziwy paralelizm
rab,
5

Futures moduł ma submodule nazwie przyłączyć że mam ochotę użytku:

Łączy wywołania asynchroniczne razem, podobnie jak w pthread_joinprzypadku wątków.

Plik Readme pokazuje kilka dobrych przykładów używania go w stylu dowolnym lub przyszłego modułu podrzędnego przy użyciu wzorca Promise. Przykład z dokumentów:

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);
Krzykliwy
źródło
2

Możliwe byłoby proste rozwiązanie tutaj: http://howtonode.org/control-flow-part-ii przewiń do Akcje równoległe. Innym sposobem byłoby, aby wszystkie A, B i C miały tę samą funkcję zwrotną, aby ta funkcja miała globalny lub przynajmniej poza funkcją inkrementor, jeśli wszystkie trzy wywoływały wywołanie zwrotne, pozwól mu działać D, oczywiście będziesz musiał także gdzieś przechowywać wyniki A, B i C.

Alex
źródło
0

Oprócz popularnych obietnic i biblioteki asynchronicznej istnieje trzeci elegancki sposób - użycie „okablowania”:

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
źródło