Является ли Node.js нативной обработкой Promise.all параллельно или последовательно?

174

Я хотел бы прояснить этот момент, так как документация не слишком ясна по этому поводу;

Q1:Promise.all(iterable) выполняется ли обработка всех обещаний последовательно или параллельно? Или, более конкретно, это эквивалент выполнения цепных обещаний, таких как

p1.then(p2).then(p3).then(p4).then(p5)....

или это какой - то другой вид алгоритма , где все p1, p2, p3, p4, p5и т.д. называют одновременно (параллельно) и результаты возвращаются , как только все решимостью (или одного брака)?

Q2: Если Promise.allработает параллельно, есть ли удобный способ запустить итерацию последовательно?

Примечание : я не хочу использовать Q или Bluebird, но все нативные спецификации ES6.

Яник Рошон
источник
Вы спрашиваете о реализации узла (V8) или о спецификации?
Амит
1
Я уверен, что Promise.allвыполняет их параллельно.
Royhowie
@Amit я пометил, node.jsи так io.jsкак это, где я использую это. Так что, да, реализация V8, если хотите.
Яник Рошон
9
Обещания не могут быть «выполнены». Они начинают свою задачу, когда они создаются - они представляют только результаты - и вы выполняете все параллельно, даже прежде чем передать их Promise.all.
Берги
Обещания выполняются в момент создания. (можно подтвердить, запустив немного кода). В new Promise(a).then(b); c();выполняется первым, а затем с, то б. Это не Promise.all, который выполняет эти обещания, он просто обрабатывает, когда они разрешаются.
Mateon1

Ответы:

258

Promise.all(iterable)Выполняет ли все обещания?

Нет, обещания не могут быть «выполнены». Они начинают свою задачу, когда они создаются - они представляют только результаты - и вы выполняете все параллельно, даже прежде чем передать их Promise.all.

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

Есть ли удобный способ запустить итерацию последовательно?

Если у вас уже есть свои обещания, вы ничего не можете сделать, но Promise.all([p1, p2, p3, …])(который не имеет понятия последовательности). Но если у вас есть итеративные асинхронные функции, вы действительно можете запускать их последовательно. В основном вам нужно получить от

[fn1, fn2, fn3, …]

в

fn1().then(fn2).then(fn3).then(…)

и решение для этого использует Array::reduce:

iterable.reduce((p, fn) => p.then(fn), Promise.resolve())
Берги
источник
1
В этом примере, итерируем ли массив функций, которые возвращают обещание, которое вы хотите вызвать?
Джеймс Реатеги
2
@SSHThis: Это в точности как thenпоследовательность - возвращаемое значение является обещанием для последнего fnрезультата, и вы можете связать другие обратные вызовы с этим.
Берги
1
@wojjas Это точно эквивалентно fn1().then(p2).then(fn3).catch(…? Не нужно использовать выражение функции.
Берги
1
@wojjas Конечно, retValFromF1это передается p2, это именно то, что p2делает. Конечно, если вы хотите сделать больше (передать дополнительные переменные, вызвать несколько функций и т. Д.), Вам нужно использовать выражение функции, хотя изменение p2в массиве будет проще
Bergi
1
@ robe007 Да, я имел в виду, что iterableэто [fn1, fn2, fn3, …]массив
Берги
62

В параллели

await Promise.all(items.map(async item => { await fetchItem(item) }))

Преимущества: быстрее. Все итерации будут выполнены, даже если одна не удалась.

В последовательности

for (let i = 0; i < items.length; i++) {
    await fetchItem(items[i])
}

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

david_adler
источник
7
Или:for (const item of items) await fetchItem(item);
Роберт Пеннер
1
@david_adler В параллельном примере преимуществ вы сказали, что все итерации будут выполнены, даже если одна из них не удалась . Если я не ошибаюсь, это все равно быстро провалится. Чтобы изменить это поведение, можно сделать что-то вроде: await Promise.all(items.map(async item => { return await fetchItem(item).catch(e => e) }))
Taimoor
@Taimoor да, он «быстро проваливается» и продолжает выполнять код после Promise.all, но все итерации по-прежнему выполняются codepen.io/mfbx9da4/pen/BbaaXr
david_adler
Этот подход лучше, когда asyncфункция является вызовом API и вы не хотите DDOS сервера. Вы лучше контролируете отдельные результаты и ошибки, возникающие при выполнении. Более того, вы можете решить, какие ошибки продолжать и что прерывать цикл.
мандарин
Обратите внимание, что на самом деле javascript не выполняет асинхронные запросы "параллельно" с использованием потоков, поскольку javascript является однопоточным. developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
david_adler
11

Ответ Bergis вывел меня на правильный путь, используя Array.reduce.

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

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

Вот чем я закончил.

getAllFiles().then( (files) => {
    return files.reduce((p, theFile) => {
        return p.then(() => {
            return transferFile(theFile); //function returns a promise
        });
    }, Promise.resolve()).then(()=>{
        console.log("All files transferred");
    });
}).catch((error)=>{
    console.log(error);
});

Как предлагают предыдущие ответы, используя:

getAllFiles().then( (files) => {
    return files.reduce((p, theFile) => {
        return p.then(transferFile(theFile));
    }, Promise.resolve()).then(()=>{
        console.log("All files transferred");
    });
}).catch((error)=>{
    console.log(error);
});

не дождался завершения передачи, прежде чем начать другую, а также текст «Все файлы передан» появился еще до начала первой передачи файла.

Не уверен, что я сделал не так, но хотел поделиться тем, что сработало для меня.

Изменить: так как я написал этот пост, я теперь понимаю, почему первая версия не работает. then () ожидает функцию, возвращающую обещание. Итак, вы должны передать имя функции без скобок! Теперь моей функции нужен аргумент, поэтому мне нужно добавить анонимную функцию без аргументов!

tkarls
источник
4

просто чтобы уточнить ответ @ Bergi (что очень лаконично, но сложно понять;)

Этот код будет запускать каждый элемент в массиве и добавлять следующую «цепочку» в конец;

function eachorder(prev,order) {
        return prev.then(function() {
          return get_order(order)
            .then(check_order)
            .then(update_order);
        });
    }
orderArray.reduce(eachorder,Promise.resolve());

надеюсь, что это имеет смысл.

TimoSolo
источник
3

Вы также можете последовательно обработать итерацию с помощью асинхронной функции, используя рекурсивную функцию. Например, заданный массив aдля обработки с асинхронной функцией someAsyncFunction():

var a = [1, 2, 3, 4, 5, 6]

function someAsyncFunction(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("someAsyncFunction: ", n)
      resolve(n)
    }, Math.random() * 1500)
  })
}

//You can run each array sequentially with: 

function sequential(arr, index = 0) {
  if (index >= arr.length) return Promise.resolve()
  return someAsyncFunction(arr[index])
    .then(r => {
      console.log("got value: ", r)
      return sequential(arr, index + 1)
    })
}

sequential(a).then(() => console.log("done"))

Марк Мейер
источник
использование array.prototype.reduceнамного лучше с точки зрения производительности, чем рекурсивная функция
Матеуш Совиньски
@ MateuszSowiński, время ожидания каждого вызова составляет 1500 мс. Учитывая, что это делает асинхронные вызовы последовательно, трудно понять, насколько это актуально, даже для очень быстрого асинхронного решения.
Марк Мейер
Допустим, вам нужно выполнить 40 действительно быстрых асинхронных функций друг за другом - использование рекурсивных функций очень быстро засорит вашу память
Mateusz Sowiński
@ MateuszSowiński, что стек здесь не заканчивается ... мы возвращаемся после каждого звонка. Сравните это с тем, reduceгде вы должны построить всю then()цепочку за один шаг, а затем выполнить.
Марк Мейер
При 40-м вызове последовательной функции первый вызов функции все еще находится в памяти, ожидая возврата цепочки последовательных функций
Mateusz Sowiński
3

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

Параллельный против Concurent

Фактически, что Promise.allпроисходит, складывая функцию обещаний в соответствующей очереди (см. Архитектуру цикла событий), одновременно выполняя их (вызывая P1, P2, ...), затем ожидая каждого результата, затем разрешая Promise.all со всеми обещаниями полученные результаты. Promise.all потерпит неудачу при первом неисполнении обещания, если вы сами не справитесь с отклонением.

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

Наконец, чтобы ответить на ваш вопрос, Promise.allне будем выполнять ни параллельно, ни последовательно, а одновременно.

Адриен Де Перетти
источник
Это неправильно. NodeJS может запускать вещи параллельно. У NodeJS есть концепция рабочего потока. По умолчанию количество рабочих потоков равно 4. Например, если вы используете криптографическую библиотеку для хеширования двух значений, вы можете выполнять их параллельно. Два рабочих потока будут обрабатывать задачу. Конечно, ваш процессор должен быть многоядерным для поддержки параллелизма.
Шихаб
Да, вы правы, это то, что я сказал в конце первого абзаца, но я говорил о детском процессе, конечно, они могут работать на рабочих.
Адриан Де Перетти
2

Использование асинхронного ожидания массива обещаний может быть легко выполнено последовательно:

let a = [promise1, promise2, promise3];

async function func() {
  for(let i=0; i<a.length; i++){
    await a[i]();
  }  
}

func();

Примечание. В приведенной выше реализации, если обещание отклонено, остальное не будет выполнено. Если вы хотите, чтобы все ваши обещания были выполнены, оберните await a[i]();внутреннюю частьtry catch

Аян
источник
2

параллельно

увидеть этот пример

const resolveAfterTimeout = async i => {
  return new Promise(resolve => {
    console.log("CALLED");
    setTimeout(() => {
      resolve("RESOLVED", i);
    }, 5000);
  });
};

const call = async () => {
  const res = await Promise.all([
    resolveAfterTimeout(1),
    resolveAfterTimeout(2),
    resolveAfterTimeout(3),
    resolveAfterTimeout(4),
    resolveAfterTimeout(5),
    resolveAfterTimeout(6)
  ]);
  console.log({ res });
};

call();

запустив код, он утешит «ВЫЗОВ» для всех шести обещаний, а когда они будут решены, он будет утешать каждые 6 ответов после тайм-аута одновременно

Чинтан Раджпара
источник
1

Ответ Берги помог мне сделать вызов синхронным. Я добавил ниже пример, где мы вызываем каждую функцию после вызова предыдущей функции.

function func1 (param1) {
    console.log("function1 : " + param1);
}
function func2 () {
    console.log("function2");
}
function func3 (param2, param3) {
    console.log("function3 : " + param2 + ", " + param3);
}

function func4 (param4) {
    console.log("function4 : " + param4);
}
param4 = "Kate";

//adding 3 functions to array

a=[
    ()=>func1("Hi"),
    ()=>func2(),
    ()=>func3("Lindsay",param4)
  ];

//adding 4th function

a.push(()=>func4("dad"));

//below does func1().then(func2).then(func3).then(func4)

a.reduce((p, fn) => p.then(fn), Promise.resolve());
Nithi
источник
Это ответ на оригинальный вопрос?
Джулио Качинь
0

Вы можете сделать это за цикл.

Асинхронная функция возвращает обещание

async function createClient(client) {
    return await Client.create(client);
}

let clients = [client1, client2, client3];

если вы пишете следующий код, то клиент создается параллельно

const createdClientsArray = yield Promise.all(clients.map((client) =>
    createClient(client);
));

тогда все клиенты создаются параллельно. но если вы хотите создать клиент последовательно, то вы должны использовать для цикла

const createdClientsArray = [];
for(let i = 0; i < clients.length; i++) {
    const createdClient = yield createClient(clients[i]);
    createdClientsArray.push(createdClient);
}

тогда все клиенты создаются последовательно.

счастливого кодирования :)

Дипак Сисодия
источник
8
В настоящее время async/ awaitдоступен только с транспортером или с использованием других двигателей, кроме Node. Кроме того, вы действительно не должны смешиваться asyncс yield. В то время как они действуют одинаково с транспортерами co, они действительно совершенно разные и не должны обычно заменять друг друга. Кроме того, вы должны упомянуть эти ограничения, так как ваш ответ вводит в заблуждение начинающих программистов.
Яник Рошон
0

Я использовал для того, чтобы решить последовательные обещания. Я не уверен, поможет ли это здесь, но это то, чем я занимался.

async function run() {
    for (let val of arr) {
        const res = await someQuery(val)
        console.log(val)
    }
}

run().then().catch()
Ник Котенберг
источник
0

это может ответить на часть вашего вопроса.

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

function tester1(a) {
  return new Promise(function(done) {
    setTimeout(function() {
      done(a + 1);
    }, 1000);
  })
}

function tester2(a) {
  return new Promise(function(done) {
    setTimeout(function() {
      done(a * 5);
    }, 1000);
  })
}

function promise_chain(args, list, results) {

  return new Promise(function(done, errs) {
    var fn = list.shift();
    if (results === undefined) results = [];
    if (typeof fn === 'function') {
      fn(args).then(function(result) {
        results.push(result);
        console.log(result);
        promise_chain(result, list, results).then(done);
      }, errs);
    } else {
      done(results);
    }

  });

}

promise_chain(0, [tester1, tester2, tester1, tester2, tester2]).then(console.log.bind(console), console.error.bind(console));

это я
источник
0

Я наткнулся на эту страницу, пытаясь решить проблему в NodeJS: повторную сборку файловых блоков. В основном: у меня есть массив имен файлов. Мне нужно добавить все эти файлы в правильном порядке, чтобы создать один большой файл. Я должен сделать это асинхронно.

Модуль Node 'fs' предоставляет appendFileSync, но я не хотел блокировать сервер во время этой операции. Я хотел использовать модуль fs.promises и найти способ связать все это вместе. Примеры на этой странице мне не помогли, потому что на самом деле мне потребовалось две операции: fsPromises.read () для чтения в чанке файла и fsPromises.appendFile () для конкатенации в файл назначения. Возможно, если бы я был лучше с javascript, я мог бы заставить предыдущие ответы работать на меня. ;-)

Я наткнулся на это ... https://css-tricks.com/why-using-reduce-to-sequentially-resolve-promises-works/ ... и я смог взломать рабочее решение.

TLDR:

/**
 * sequentially append a list of files into a specified destination file
 */
exports.append_files = function (destinationFile, arrayOfFilenames) {
    return arrayOfFilenames.reduce((previousPromise, currentFile) => {
        return previousPromise.then(() => {
            return fsPromises.readFile(currentFile).then(fileContents => {
                return fsPromises.appendFile(destinationFile, fileContents);
            });
        });
    }, Promise.resolve());
};

И вот жасмин юнит-тест для него:

const fsPromises = require('fs').promises;
const fsUtils = require( ... );
const TEMPDIR = 'temp';

describe("test append_files", function() {
    it('append_files should work', async function(done) {
        try {
            // setup: create some files
            await fsPromises.mkdir(TEMPDIR);
            await fsPromises.writeFile(path.join(TEMPDIR, '1'), 'one');
            await fsPromises.writeFile(path.join(TEMPDIR, '2'), 'two');
            await fsPromises.writeFile(path.join(TEMPDIR, '3'), 'three');
            await fsPromises.writeFile(path.join(TEMPDIR, '4'), 'four');
            await fsPromises.writeFile(path.join(TEMPDIR, '5'), 'five');

            const filenameArray = [];
            for (var i=1; i < 6; i++) {
                filenameArray.push(path.join(TEMPDIR, i.toString()));
            }

            const DESTFILE = path.join(TEMPDIR, 'final');
            await fsUtils.append_files(DESTFILE, filenameArray);

            // confirm "final" file exists    
            const fsStat = await fsPromises.stat(DESTFILE);
            expect(fsStat.isFile()).toBeTruthy();

            // confirm content of the "final" file
            const expectedContent = new Buffer('onetwothreefourfive', 'utf8');
            var fileContents = await fsPromises.readFile(DESTFILE);
            expect(fileContents).toEqual(expectedContent);

            done();
        }
        catch (err) {
            fail(err);
        }
        finally {
        }
    });
});

Надеюсь, это кому-нибудь поможет.

сойка
источник