Координация параллельного выполнения в node.js

Управляемая событиями модель программирования node.js затрудняет координацию потока программы.

Простое последовательное выполнение превращается в вложенные обратные вызовы, что достаточно просто (хотя и немного свернуто для записи).

Но как насчет параллельного выполнения? Скажем, у вас есть три задачи A, B, C, которые могут выполняться параллельно, и когда они будут выполнены, вы хотите отправить их результаты в задачу D.

С моделью fork/join это будет

  • fork A
  • fork B
  • fork C
  • присоединиться к A, B, C, запустить D

Как написать это в node.js? Есть ли передовая практика или кулинарные книги? Нужно ли мне рулон решение каждый раз, или есть библиотека с помощниками для этого?

Ответ 1

Ничто не является действительно параллельным в node.js, поскольку оно однопоточное. Однако несколько событий можно планировать и запускать в последовательности, которую вы не можете определить заранее. И некоторые вещи, такие как доступ к базе данных, фактически "параллельны" тем, что сами запросы к базе данных выполняются в отдельных потоках, но при завершении их реинтегрируются в поток событий.

Итак, как вы планируете обратный вызов для нескольких обработчиков событий? Ну, это один из распространенных методов, используемых в анимации в браузере javascript: используйте переменную для отслеживания завершения.

Это звучит как хак, и это так, и это звучит потенциально беспорядочно, оставляя кучу глобальных переменных вокруг отслеживания и на меньшем языке. Но в javascript мы можем использовать закрытие:

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

В приведенном выше примере мы сохраняем код простым, предполагая, что функции async и callback не требуют аргументов. Разумеется, вы можете модифицировать код для передачи аргументов в функции async, а функция обратного вызова накапливает результаты и передает их функции shared_callback.


Дополнительный ответ:

Фактически, даже как есть, эта функция fork() уже может передавать аргументы для асинхронных функций с использованием замыкания:

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

единственное, что осталось сделать - это накопить результаты от A, B, C и передать их D.


Еще один дополнительный ответ:

Я не мог сопротивляться. Остерегался думать об этом во время завтрака. Здесь выполняется реализация fork() которая накапливает результаты (обычно передаваемые как аргументы функции обратного вызова):

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

Это было достаточно легко. Это делает fork() довольно общей целью и может использоваться для синхронизации нескольких неоднородных событий.

Пример использования в 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);

Обновить

Этот код был написан до существования библиотек, таких как async.js или различные библиотеки, основанные на обещаниях. Я хотел бы поверить, что async.js был вдохновлен этим, но у меня нет никаких доказательств этого. В любом случае.. если вы подумываете об этом сегодня, взгляните на async.js или обещания. Просто рассмотрите ответ выше хорошего объяснения/иллюстрации того, как такие вещи, как async.parallel work.

Для полноты async.parallel следующим образом: как вы это сделаете с помощью async.parallel:

var async = require('async');

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

Обратите внимание, что async.parallel работает точно так же, как и функция fork реализованная выше. Основное различие заключается в том, что он передает ошибку в качестве первого аргумента D и обратного вызова в качестве второго аргумента в соответствии с соглашением node.js.

Используя обещания, мы напишем его следующим образом:

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

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

Ответ 2

Я считаю, что теперь модуль async предоставляет эту параллельную функциональность и примерно такой же, как и функция fork выше.

Ответ 3

Модуль futures имеет подмодуль под названием join, который мне понравился:

Объединение асинхронных вызовов вместе с тем, как pthread_join работает для потоков.

В readme показаны некоторые хорошие примеры использования этого фристайла или с использованием подмодуля future с использованием шаблона Promise. Пример из документов:

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

Ответ 4

Здесь может быть возможно простое решение: http://howtonode.org/control-flow-part-ii перейдите к Параллельные действия. Другим способом было бы, чтобы все A, B и C имели одну и ту же функцию обратного вызова, имеют ли эта функция глобальный или, по крайней мере, внекорпоративный инкремент, если все три вызвали обратный вызов, тогда пусть это запустит D, конечно, вам также нужно будет хранить результаты A, B и C.

Ответ 7

В дополнение к популярной promises и асинхронной библиотеке существует 3-й элегантный способ - с помощью "проводки":

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