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