ES7 асинхронная / жду концептуальная проблема

Я мигрирую существующую программу для использования async/await (с помощью Babel"s bluebirdCoroutines) чтобы выучить этот стиль. Я смотрел на этот учебник.

Я немного обеспокоен следующим поведением. Этот фрагмент работает, как и ожидалось:

let parts = [];
let urlsP = urls.map((url, index) => { 
    return dlPart(url, index, tempDir); 
});
for (let urlP of urlsP) { // Parallel (yay!)
    parts.push(await urlP);
}
for (let part of parts) { // Sequential
    await appendFile(leFile, part);
}

Переписан следующим образом, он все еще работает, но первая операция уже не параллельна (для завершения требуется гораздо больше времени)!

let index = 0;
let parts = [];
for (let url of urls) { // NOT Parallel any more!!!
    index++;
    parts.push(await dlPart(url, index, tempDir));
}
for (let part of parts) {
    await appendFile(leFile, part);
}

Это реализация dlPart()

function dlPart(url, num, dir) {
    var cmd = 'wget --quiet "' + url + '" -O ' + dir + "/" + num;
    return exec(cmd).then(() => {
        return dir + '/' + num;
    });
}

Что мне не хватает?

3 ответа

Решение

.map Функция асинхронна, поэтому остальной код не должен ждать, она будет завершена, когда будет готова. Затем вы заменили его for loop это сдерживает все, пока оно завершается.

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

В первом примере вы запускаете все Обещания, которые начинают выполнять свои функции. Тогда в этом цикле:

for (let urlP of urlsP) { // Parallel (yay!)
    parts.push(await urlP);
}

Вы ждете выполнения первого обещания, а затем второго обещания и т. Д. Но все время, пока вы ожидаете выполнения первого обещания, ВСЕ другие обещания все еще выполняются. Следовательно, они работают "параллельно".

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

for (let url of urls) { // NOT Parallel any more!!!
    index++;
    parts.push(await dlPart(url, index, tempDir));
}

parts.push строка делает следующее по порядку:

  1. Запускается dlPart() который возвращает обещание и начинает загрузку части
  2. Ждет обещание разрешить
  3. Вставляет разрешенное значение в parts,

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

Замечания: .map не асинхронный, если бы это было так, ваш первый пример не работал бы с большими списками, потому что for of цикл начнется, прежде чем все обещания были добавлены к вашему urlsP массив.

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

Для всех намерений и целей это именно то, что Сэм объяснил, но способ, который я нахожу, помогает разработчикам понять это таким образом, к которому они более привыкли.

Код ES7

let parts = [];
let urlsP = urls.map((url, index) => { 
    return dlPart(url, index, tempDir); 
});
for (let urlP of urlsP) { // Parallel (yay!)
    parts.push(await urlP);
}

Код ES6

let parts = [];
// Capture all the pending promises into an array
let urlsP = urls.map((url,index)=>{
    // Returns the promise to the urlsP array
    return dlPart(url,index,tempDir);
});
// Catch all the data in an array
Promise.all(urlsP).then(res=>{
    parts=res;
});

Чтобы повторить то, что Сэм объяснил пост выше. В примере ES7 функция map вызывает все асинхронные функции и создает новый массив обещаний. for of loop перебирает массив обещаний и проверяет, было ли обещание выполнено, если оно еще не выполнено, он дождется разрешения конкретного обещания, а затем повторите этот процесс. Если бы вам удалось просмотреть этот код в замедленном режиме с помощью тега отладчика в chrome, вы заметили бы, что некоторые обещания будут выполнены к тому времени, когда цикл проверяет, был ли он решен, в то время как другие вам придется ждать.

Пример ES6 по сути тот же, с той лишь разницей, как мы получаем массив частей. В этом случае ответ Promise all является массивом всех разрешенных значений, поэтому мы делаем части равными ответу, а не помещаем его в массив


Теперь представьте, что вы пишете следующий код в es6:

Код ES7

let index = 0; let parts = []; 
for (let url of urls) { // NOT Parallel any more!!!
    index++;
    parts.push(await dlPart(url, index, tempDir)); 
}

Вам придется использовать генератор или рекурсивную функцию. Мое понимание генераторов все еще довольно новое, поэтому я покажу рекурсивную функцию

Код ES5/6

let index = 0; let parts = []; let promises = [];

function getParts(index,){
      return new Promise((resolve,reject)=>{
            dlPart(urls[index],index,tempDir).then(res=>{
                parts.push(res)
                if(index<urls.length-1){
                    promises.push(getParts(++index));
                    resolve()
                }else{
                    resolve()
                }
            }
      }
    }
promises.push(getParts(index));
Promise.all(promises).then(finished=>{
    // Then you can continue with whatever code
});

Теперь с этим примером кода ES7 выше вы заметите, что for of loop выполняет итерацию по массиву urls и ожидает разрешения, прежде чем перейти к следующему индексу массива.

Пример ES6 делает то же самое в том смысле, что он начинается с URL- адреса с индексом 0, ожидает dlPart обещание разрешить, помещает ответ в массив частей, проверяет, что индекс все еще меньше, чем длина массива urls, затем getParts вызывает себя снова, пока, наконец, не исчерпает индексы urls и не решит свое последнее обещание, так что код ниже Promise.all(promises) может начать бежать

Когда вы начнете смотреть на различия в удобочитаемости между ES6 и ES7, вы поймете, почему async/await был завершен в спецификации es7.

Другие вопросы по тегам