Чи обробляється Node.js рідним Promise.all паралельно або послідовно?


173

Я хотів би уточнити цей момент, оскільки документація щодо нього не надто чітка;

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();a виконується спочатку, потім c, потім b. Це не Promise.all, який виконує ці обіцянки, він просто обробляє, коли вони вирішуються.
Mateon1

Відповіді:


257

Чи 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
У цьому прикладі чи є ітерабельним масив функцій, які повертають обіцянку, яку ви хочете зателефонувати?
James Reategui

2
@SSHTЦе: Це точно так само, як thenпослідовність - повернене значення - це обіцянка для останнього fnрезультату, і ви можете зв’язати з цим інші зворотні виклики.
Бергі

1
@wojjas Це рівнозначно fn1().then(p2).then(fn3).catch(…? Не потрібно використовувати вираз функції.
Бергі

1
@wojjas Звичайно, retValFromF1це передається в p2саме p2це. Звичайно, якщо ви хочете зробити більше (передавати додаткові змінні, викликати кілька функцій тощо), вам потрібно використовувати вираження функції, хоча змінити p2масив було б простіше
Бергі,

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])
}

Переваги: ​​Змінні в циклі можуть бути спільними для кожної ітерації. Поводиться як звичайний імперативний синхронний код.


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

Відповідь Бергі зібрала мене на правильному шляху за допомогою 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);
});

не чекав завершення передачі перед початком іншого, а також текст "Усі передані файли" надходив ще до того, як почалася перша передача файлів.

Не впевнений, що я зробив не так, але хотів поділитися тим, що працювало на мене.

Редагувати: з моменту написання цього повідомлення я тепер розумію, чому перша версія не працювала. тоді () очікує, що функція повертає обіцянку. Отже, вам слід передати ім'я функції без дужок! Тепер моя функція хоче аргументу, тому мені потрібно загорнути анонімну функцію, не беручи аргументів!


4

просто уточнити відповідь @ Бергі (що дуже лаконічно, але складно зрозуміти;)

Цей код запустить кожен елемент у масиві та додасть наступний ', а потім ланцюжок' до кінця;

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

сподівання, що має сенс.


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набагато краще з точки зору продуктивності, ніж рекурсивна функція
Mateusz Sowiński

@ MateuszSowiński, між кожним викликом є ​​час очікування 1500 мс. Зважаючи на те, що це виконує асинхронні дзвінки послідовно, важко зрозуміти, наскільки це актуально навіть для дуже швидкого повороту асинхронізації.
Марк Мейєр

Скажімо, вам потрібно виконати 40 дійсно швидких функцій асинхронізації один за одним - використання рекурсивних функцій досить швидко засмітить вашу пам'ять
Матеуш Совінський

@ MateuszSowiński, що стек тут не закінчується ... ми повертаємося після кожного дзвінка. Порівняйте це з тим, reduceде вам потрібно скласти весь then()ланцюг за один крок, а потім виконати.
Марк Мейєр

У 40-му виклику послідовної функції перший виклик функції все ще залишається в пам'яті, чекаючи повернення ланцюга послідовних функцій
Матеуш Совінський

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

запустивши код, він консолірує "CALLED" на всі шість обіцянок, і коли вони будуть вирішені, він консолірує кожні 6 відповідей після таймауту одночасно


2

NodeJS не виконує обіцянки паралельно, він виконує їх одночасно, оскільки це єдина архітектура циклу подій. Існує можливість запускати речі паралельно, створюючи новий дочірній процес, щоб скористатися багатоядерним процесором.

Паралельний V-конспект

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

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

Нарешті, щоб відповісти на ваше запитання, Promise.allвиконується не паралельно, ні послідовно, але одночасно.


Це неправильно. NodeJS може працювати паралельно. NodeJS має концепцію робочої нитки. За замовчуванням кількість робочих потоків становить 4. Наприклад, якщо ви використовуєте бібліотеку криптовалют для хешування двох значень, ви можете виконувати їх паралельно. Дві робочі нитки впораються із завданням. Звичайно, ваш процесор повинен бути багатоядерним, щоб підтримувати паралелізм.
Шихаб

Так, ви прав, це те, що я сказав в кінці першого пункту, але я говорив про дитячий процес, звичайно, вони можуть працювати на робочих місцях.
Адріан Де Перетті

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

Це відповідь на початкове запитання?
Джуліо Каччін

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: повторна збірка фрагментів файлів. В основному: у мене є масив імен файлів. Мені потрібно додати всі ці файли у правильному порядку, щоб створити один великий файл. Я повинен це робити асинхронно.

Модуль '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 {
        }
    });
});

Я сподіваюся, що це комусь допоможе.

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