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

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

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

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

С моделью вилки / соединения это будет

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

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


person Thilo    schedule 08.01.2011    source источник


Ответы (7)


В 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 не требуют аргументов. Вы, конечно, можете изменить код, чтобы передавать аргументы асинхронным функциям, а функция обратного вызова накапливает результаты и передает их в функцию 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 или promises. Просто рассмотрите ответ выше, хорошее объяснение / иллюстрацию того, как работают такие вещи, как 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);
person slebetman    schedule 08.01.2011
comment
В node.js нет ничего по-настоящему параллельного, поскольку он однопоточный. Не правда. Все, что не использует ЦП (например, ожидание ввода-вывода по сети), выполняется параллельно. - person Thilo; 08.01.2011
comment
По большей части это правда. Ожидание ввода-вывода в Node не блокирует выполнение другого кода, но когда код запускается, он выполняется по одному. Единственное настоящее параллельное выполнение в Node - это порождение дочерних процессов, но тогда это можно сказать практически о любой среде. - person MooGoo; 08.01.2011
comment
@Thilo: Обычно мы называем код, который не использует ЦП, неработающим. Если вы не работаете, вы не можете работать параллельно. - person slebetman; 08.01.2011
comment
@MooGoo: создание дочернего процесса или потока потенциально может выполняться в истинно параллельном режиме: в многоядерных системах. В наши дни многоядерные системы стали нормой. Фактически, даже смартфоны, выходящие позже в этом году, начинают быть многоядерными. В этом разница между потоками / процессами и событиями. Потоки могут или не могут работать параллельно, но события определенно не работают параллельно. - person slebetman; 08.01.2011
comment
@MooGoo: Смысл этого в том, что с событиями, поскольку мы знаем, что они определенно не могут работать параллельно, нам не нужно беспокоиться о семафорах и мьютексах, в то время как с потоками мы должны блокировать общие ресурсы. - person slebetman; 08.01.2011
comment
@MooGoo @Thilo: На самом деле я возвращаюсь к определению истинной параллельности. Реальная разница между запуском параллельных потоков и планированием событий заключается в том, что с потоками ваш код может быть прерван в любое время (например, завершением ввода-вывода), в то время как с событиями ничто не может прервать ваш код. Вы можете проиллюстрировать, что узел не выполняет код параллельно, написав бесконечный цикл: while(1){} и увидев, что все другие задачи не будут выполняться до тех пор, пока цикл не завершится, чего никогда не бывает. Потоки ведут себя иначе. - person slebetman; 08.01.2011
comment
Обычно мы называем код, который не использует ЦП, неработающим. Ну, он может использовать ЦП в других системах. Когда я звоню в веб-службу, реальная работа выполняется, пока я жду. С помощью node.js я могу вызывать десять из этих веб-сервисов параллельно, что увеличивает мою пропускную способность в десять раз, даже если на моем локальном процессоре выполняется только один поток. Конечно, это рассуждение работает только для задач, которые не связаны с процессором, но для них node.js, вероятно, в любом случае не подходит. Для них вам нужна превентивная многозадачность и многоядерная поддержка с использованием нескольких потоков или нескольких процессов. - person Thilo; 08.01.2011
comment
@Thilo: Я уже говорил об этом в своем первоначальном ответе. Вы процитировали Nothing is truly parallel in node.js since it is single threaded.., но не цитировали And some things like database access are actually "parallel".., который говорит в точности то, что вы говорите. - person slebetman; 08.01.2011
comment
+1 Мне нравится, что улучшенная fork функция. Имея способ сделать порядок в all_results таким же, как порядок в async_calls (сейчас это неопределенный порядок завершения), это выглядит как принятый ответ ;-) - person Thilo; 08.01.2011
comment
Что касается семантики Nothing is truly parallel and some things are parallel, возможно, мы сможем договориться о Everything is parallel in node.js except CPU use. - person Thilo; 08.01.2011
comment
Изменил свой ответ, чтобы порядок all_results был таким же, как порядок async_calls. - person slebetman; 08.01.2011
comment
На самом деле я просто говорил о параллелизме на уровне ОС, независимо от того, имеет ли он место на самом деле на аппаратном уровне. Nodejs никогда не был связан с параллельным запуском кода, а только с кодом ввода-вывода, который не блокирует выполнение другого кода в ожидании ответа сервера / файловой системы. Я часто вызываю дочерний процесс Node для выполнения задачи (например, запросов к базе данных), которая обычно блокирует выполнение кода. Это зависит от вытесняющей многозадачности на уровне ОС, чтобы разделить время процессора и не позволить одному процессу занимать больше, чем его справедливая доля, и происходит независимо от способности вашего процессора выполнять настоящую параллельную обработку. - person MooGoo; 08.01.2011
comment
@slebetman: Не могли бы вы отредактировать свой первый пример, чтобы показать полное использование? - person TK-421; 08.01.2011
comment
@ luke-in-stormtrooper-armor: Какой пример тебе нужен? В среде браузера? В узле? - person slebetman; 09.01.2011
comment
@ tk421: Кстати, использование в первом примере завершено, если вы определили функции A, B, C и D. - person slebetman; 09.01.2011
comment
@ tk421: Хорошо, внизу добавлен более полный пример узла. Сохранено в стиле A, B, C, D, чтобы гармонировать с тоном остальной части ответа. Конечно, вы можете написать его полностью с помощью анонимных функций. - person slebetman; 09.01.2011
comment
Правильно ли я говорю, что это не функции, выполняющиеся параллельно, но они (в лучшем случае) выполняются в неопределенной последовательности с кодом, не прогрессирующим до тех пор, пока не вернется каждый «async_func»? - person Aaron Rustad; 10.01.2011
comment
@BigCanOfTuna: Я бы сказал, что да. Это мое несогласие с ОП. Мое определение параллельности (и я думаю, что ваше тоже) исключает то, что мы здесь делаем. Кстати, структура управления fork / join берет свое начало в инструкциях scatter / gather на оборудовании Cray и в наши дни более популярно реализуется как map- ›reduce. Лично я предпочитаю вызывать функцию scatter() или sync(), потому что fork имеет другое значение на уровне ОС. - person slebetman; 10.01.2011
comment
Я думаю, что Slebetman и @Thilo действительно спорят между параллельной обработкой и параллельной обработкой. См. stackoverflow.com/a/1898024/1995977, чтобы увидеть потрясающую диаграмму разницы. - person Steve Jansen; 03.01.2014
comment
// Читать 3 файла параллельно и обрабатывать их вместе: как это может быть параллельно, если он однопоточный? Я не вижу, чтобы вы создавали какие-либо новые потоки в своей функции fork. - person user1767586; 14.04.2015
comment
@ user1767586: Да, они однопоточные. Если вы хотите подробно узнать, как это работает, ознакомьтесь с функцией select() в C. - person slebetman; 14.04.2015
comment
@ user1767586: В основном они параллельны, потому что фактическая обработка обычно выполняется на других машинах, часто в другой стране. Ожидая ответа от этих машин, вы можете запустить другой код. Вы также можете ждать параллельно, то есть инициировать множество запросов и ждать их всех вместе. Конечно, я преувеличиваю, когда говорю, что обработка происходит в другой стране, но ненамного. Обработка также может происходить на том же компьютере, но в другой программе (например, сервер mysql). - person slebetman; 14.04.2015

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

person Wes Gamble    schedule 24.09.2011
comment
Это неверно, async помогает организовать поток кода только в рамках одного процесса. - person bwindels; 09.04.2013
comment
async.parallel действительно делает примерно то же самое, что и вышеуказанная функция fork - person Dave Stibrany; 02.10.2013
comment
это не настоящий параллелизм - person rab; 11.10.2015

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

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

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

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);
person Randy    schedule 20.02.2012

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

person Alex    schedule 08.01.2011

Другим вариантом может быть модуль Step для узла: https://github.com/creationix/step

person Wilhelm Murdoch    schedule 30.04.2011
comment
Не похоже, что step действительно выполняет параллелизм. - person Evan Leis; 10.05.2013

Вы можете попробовать эту крошечную библиотеку: https://www.npmjs.com/package/parallel-io

person Konstantin    schedule 22.12.2014

Помимо популярных обещаний и async-библиотеки, есть 3-й элегантный способ - с помощью "wiring":

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

person Daniel Garmoshka    schedule 19.10.2015