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


79

Модель програмування node.js, керована подіями, робить дещо складніше координувати потік програми.

Просте послідовне виконання перетворюється на вкладені зворотні виклики, що досить легко (хоча трохи заплутано для запису).

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

З моделлю fork / join це було б

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

Як це написати в node.js? Чи є найкращі практики чи кулінарні книги? Чи доводиться кожен раз прокручувати рішення вручну , чи існує якась бібліотека з помічниками для цього?

Відповіді:


128

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

У наведеному вище прикладі ми робимо код простим, припускаючи, що функції асинхронізації та зворотного виклику не вимагають аргументів. Звичайно, ви можете змінити код, щоб передавати аргументи асинхронним функціям, і функція зворотного виклику накопичуватиме результати та передаватиме їх у функцію 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.

Для повноти наступне, як ви це зробите 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);

12
"У node.js нічого не є паралельно, оскільки він є однопотоковим." Неправда. Все, що не використовує центральний процесор (наприклад, очікування мережевого вводу-виводу), працює паралельно.
Тіло

3
Це правда, здебільшого. Очікування вводу-виводу в Node не блокує запуск іншого коду, але коли код запускається, це по одному. Єдине справжнє паралельне виконання в Node - це породження дочірніх процесів, але тоді це можна сказати майже про будь-яке середовище.
MooGoo

6
@Thilo: Зазвичай ми називаємо код, який не використовує центральний процесор, оскільки він не працює. Якщо ви не бігаєте, ви не можете "бігти" паралельно.
slebetman

4
@MooGoo: Наслідок цього полягає в тому, що з подіями, оскільки ми знаємо, що вони точно не можуть працювати паралельно, нам не потрібно турбуватися про семафори та мьютекси, тоді як за допомогою потоків ми повинні блокувати спільні ресурси.
slebetman

2
Чи правильно я кажу, що це не функції, що виконуються паралельно, але вони (у кращому випадку) виконуються у невизначеній послідовності, при цьому код не прогресує, доки не повернеться кожен 'async_func'?
Аарон Рустад,

10

Я вважаю, що зараз модуль "async" забезпечує цю паралельну функціональність і приблизно такий же, як і функція fork вище.


2
Це неправильно, асинхронізація лише допомагає організувати потік коду в межах одного процесу.
bwindels

2
async.parallel дійсно виконує приблизно те саме, що і вищевказана forkфункція
Дейв Стібрані

це не справжній паралелізм
rab

5

Модуль ф'ючерсів має підмодуль під назвою join, який я любив використовувати:

Об’єднує асинхронні виклики разом, подібно до того, як це pthread_joinпрацює для потоків.

Readme показує кілька хороших прикладів використання фрістайлу або використання майбутнього підмодуля за шаблоном 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);

2

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




0

На додаток до популярних обіцянок та асинхронної бібліотеки, існує 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.