Async.js - Параллельно ли параллельно?

Как я понял до сих пор: Javascript однопоточный. Если вы отложите выполнение какой-либо процедуры, вы просто планируете ее (очередь), чтобы ее запускали в следующий раз, когда поток свободен. Но Async.js определяет два метода: Async::parallel & Async::parallelLimit, и я цитирую:

  • parallel (задачи, [обратный вызов])

Выполнить массив функций параллельно, не дожидаясь завершения предыдущей функции. Если какая-либо из функций передает ошибку на обратный вызов...

  • parallelLimit (задачи, лимит, [обратный вызов])

То же, что и параллельно, задачи выполняются параллельно с максимальными "предельными" задачами, выполняемыми в любое время.

Что касается моего понимания английского языка, когда вы говорите: "выполнение задач параллельно" означает одновременное выполнение одновременно.

Как Async.js может выполнять задачи параллельно в одном потоке? Я что-то упускаю.

Ответ 1

Как Async.js может выполнять задачи параллельно в одном потоке? Я что-то пропустил.

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

как это можно включить в одном потоке?! это то, что я не мог понять.

Node.js не блокирует. Поэтому вместо параллельной обработки всех задач он переключается с одной задачи на другую. Поэтому, когда первая задача делает сам вызов ввода-вывода бездействующим, Node.js просто переключается на обработку другого.

Задачи ввода-вывода провели большую часть времени обработки, ожидая результата вызова ввода-вывода. В блокирующих языках, таких как Java, такая задача блокирует поток, пока он ждет результатов. Но Node.js использует время для обработки других задач вместо ожидания.

поэтому это означает, что если внутренняя обработка каждой задачи асинхронна, поток предоставляется каждому биту этих задач, независимо от того, закончил ли кто-нибудь из них или нет, пока все не закончили свои биты?

Да, это почти так, как вы сказали. Node.js начинает обработку первой задачи до тех пор, пока она не сделает паузу для выполнения вызова ввода-вывода. В этот момент Node.js оставляет его и предоставляет свой основной поток другой задаче. Поэтому вы можете сказать, что поток предоставляется каждой активной задаче по очереди.

Ответ 2

Функции не выполняются одновременно, но когда первая функция передается асинхронной задаче (например, setTimeout, network,...), запускается вторая, даже если первая функция не вызвала предоставленный обратный вызов.

Что касается количества параллельных задач: это зависит от того, что вы выбираете.

Ответ 3

Async.Parallel хорошо документирована здесь: https://github.com/caolan/async#parallel

Async.Parallel - это параллельные задачи ввода-вывода, а не параллельное выполнение кода. Если ваши задачи не используют таймеры или не выполняют никаких операций ввода-вывода, они будут выполняться последовательно. Любые синхронные разделы настройки для каждой задачи будут происходить один за другим. JavaScript остается однопоточным.

Ответ 4

Что касается моего понимания английского языка, когда вы говорите: "выполнение задач параллельно" означает одновременное выполнение одновременно.

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

Как Async.js может выполнять задачи параллельно в одном потоке? Я что-то пропустил.

Когда какая-то задача по какой-либо причине прекращается (например, IO), async.js выполняет другую задачу и продолжает сначала одну.

Ответ 5

Ваши сомнения имеют смысл. Прошло несколько лет с тех пор, как вы задали этот вопрос, но я думаю, что стоит добавить немного к существующим ответам.

Выполнить массив функций параллельно, не дожидаясь завершения предыдущей функции. Если какая-либо из функций передает ошибку на обратный вызов...

Это предложение не совсем корректно. На самом деле он ждет завершения каждой функции, потому что это невозможно сделать в JavaScript. Оба вызова функций и возвращаемые функции являются синхронными и блокирующими. Поэтому, когда он вызывает любую функцию, он должен ждать, пока он вернется. То, что ему не нужно ждать, - это вызов обратного вызова, который был передан этой функции.

Аллегория

Некоторое время назад я написал короткую историю, чтобы продемонстрировать эту концепцию:

Чтобы процитировать его часть:

"Итак, я сказал:" Подождите, вы скажете мне, что один пирог занимает три с половиной часа, а четыре пирога занимают всего полчаса больше, чем один? Это не имеет никакого смысла! Я, хотя, что она, должно быть, шутит поэтому я начал смеяться ".
" Но она не шутила? "" Нет, она посмотрела на меня и сказала: "Это имеет смысл. На этот раз в основном ждут, и я могу дождаться многих вещей сразу же, я прекратил смеяться и начал думать. Я делал четыре подушки в то же время, не покупал вас в любое время, возможно, было бы легче организовать, но потом снова, может быть, нет. Но на этот раз это было что-то другое. Но я еще не знал, как использовать эти знания".

Теория

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

Параллельная функция от модуля Async вызывает каждую из функций по одному, но каждая функция должна возвращаться до того, как будет вызвана следующая, нет никакого способа обойти ее. Магия здесь заключается в том, что функция не выполняет свою работу до ее возвращения - она ​​просто планирует какую-то задачу, регистрирует прослушиватель событий, передает какой-то обратный вызов где-то в другом месте, добавляет обработчик разрешения к некоторым обещаниям и т.д.

Затем, когда запланированная задача завершается, выполняется какой-либо обработчик, который ранее был зарегистрирован этой функцией, по очереди выполняет обратный вызов, который был первоначально передан модулем Async, а модуль Async знает, что эта одна из функций завершена - это время не только в том смысле, что оно вернулось, но также и то, что обратный вызов, который был передан ему, наконец был вызван.

Примеры

Итак, скажем, у вас есть 3 функции, которые загружают 3 разных URL: getA(), getB() и getC().

Мы напишем макет модуля Request для имитации запросов и некоторых задержек:

function mockRequest(url, cb) {
  const delays = { A: 4000, B: 2000, C: 1000 };
  setTimeout(() => {
    cb(null, {}, 'Response ' + url);
  }, delays[url]);
};

Теперь три функции, которые в основном одинаковы, с подробным протоколированием:

function getA(cb) {
  console.log('getA called');
  const url = 'A';
  console.log('getA runs request');
  mockRequest(url, (err, res, body) => {
    console.log('getA calling callback');
    cb(err, body);
  });
  console.log('getA request returned');
  console.log('getA returns');
}

function getB(cb) {
  console.log('getB called');
  const url = 'B';
  console.log('getB runs request');
  mockRequest(url, (err, res, body) => {
    console.log('getB calling callback');
    cb(err, body);
  });
  console.log('getB request returned');
  console.log('getB returns');
}

function getC(cb) {
  console.log('getC called');
  const url = 'C';
  console.log('getC runs request');
  mockRequest(url, (err, res, body) => {
    console.log('getC calling callback');
    cb(err, body);
  });
  console.log('getC request returned');
  console.log('getC returns');
}

И, наконец, мы вызываем их всех с помощью функции async.parallel:

async.parallel([getA, getB, getC], (err, results) => {
  console.log('async.parallel callback called');
  if (err) {
    console.log('async.parallel error:', err);
  } else {
    console.log('async.parallel results:', JSON.stringify(results));
  }
});

То, что сразу отображается, следующее:

getA called
getA runs request
getA request returned
getA returns
getB called
getB runs request
getB request returned
getB returns
getC called
getC runs request
getC request returned
getC returns

Как вы можете видеть, это все последовательные функции, вызываемые один за другим, а следующий не вызывается до того, как предыдущий возвращается. Затем мы видим это с некоторыми задержками:

getC calling callback
getB calling callback
getA calling callback
async.parallel callback called
async.parallel results: ["Response A","Response B","Response C"]

Итак, сначала закончите getC, затем getB и getC - и затем, как только закончится последний, async.parallel вызывает наш обратный вызов со всеми ответами, объединенными и в правильном порядке, - в чтобы функция была заказана нами, а не в том порядке, в котором эти запросы завершены.

Также мы видим, что программа заканчивается через 4.071 секунды, что примерно соответствует длительности самого длинного запроса, поэтому мы видим, что все запросы выполнялись одновременно.

Теперь запустите его с помощью async.parallelLimit с максимальным пределом двух параллельных задач:

async.parallelLimit([getA, getB, getC], 2, (err, results) => {
  console.log('async.parallel callback called');
  if (err) {
    console.log('async.parallel error:', err);
  } else {
    console.log('async.parallel results:', JSON.stringify(results));
  }
});

Теперь это немного другое. Мы сразу видим следующее:

getA called
getA runs request
getA request returned
getA returns
getB called
getB runs request
getB request returned
getB returns

Итак, getA и getB были вызваны и возвращены, но getC еще не был вызван. Затем после некоторой задержки мы видим:

getB calling callback
getC called
getC runs request
getC request returned
getC returns

который показывает, что, как только getB называется обратным вызовом, модуль Async больше не выполняет 2 задачи, а только 1 и может запустить другой, то есть getC, и он делает это немедленно.

Затем с другими задержками мы видим:

getC calling callback
getA calling callback
async.parallel callback called
async.parallel results: ["Response A","Response B","Response C"]

который завершает весь процесс, как в примере async.parallel. На этот раз весь процесс занял примерно 4 секунды, потому что отложенный вызов getC не имел никакого значения - ему все же удалось закончить до того, как закончил первый вызов getA.

Но если мы изменим задержки на эти:

const delays = { A: 4000, B: 2000, C: 3000 };

тогда ситуация другая. Теперь async.parrallel занимает 4 секунды, но async.parallelLimit с пределом 2 занимает 5 секунд, а порядок немного отличается.

Без ограничений:

$ time node example.js
getA called
getA runs request
getA request returned
getA returns
getB called
getB runs request
getB request returned
getB returns
getC called
getC runs request
getC request returned
getC returns
getB calling callback
getC calling callback
getA calling callback
async.parallel callback called
async.parallel results: ["Response A","Response B","Response C"]

real    0m4.075s
user    0m0.070s
sys     0m0.009s

С пределом 2:

$ time node example.js
getA called
getA runs request
getA request returned
getA returns
getB called
getB runs request
getB request returned
getB returns
getB calling callback
getC called
getC runs request
getC request returned
getC returns
getA calling callback
getC calling callback
async.parallel callback called
async.parallel results: ["Response A","Response B","Response C"]

real    0m5.075s
user    0m0.057s
sys     0m0.018s

Резюме

Я думаю, что самое важное, что нужно помнить - независимо от того, используете ли вы обратные вызовы, как в этом случае, или promises или async/await, является то, что в однопоточных циклах событий вы можете делать только одну вещь одновременно, но вы можете дождаться многих вещей одновременно.