Виклик функцій асинхронізації / очікування паралельно


431

Наскільки я розумію, в ES7 / ES2016 введення множинних awaitкодів буде працювати аналогічно ланцюгу .then()з обіцянками, тобто, вони будуть виконуватись одна за одною, а не паралельно. Так, наприклад, у нас є цей код:

await someCall();
await anotherCall();

Я правильно розумію, що anotherCall()буде викликано лише тоді, коли someCall()буде завершено? Який найелегантніший спосіб викликати їх паралельно?

Я хочу використовувати його в Node, тому, можливо, є рішення з бібліотекою async?

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


1
@adeneo Це неправильно, Javascript ніколи не працює паралельно у власному контексті.
Сліпий67,

5
@ Blindman67 - це, принаймні, так, як означає ОП, коли дві операції з асинхронізацією виконуються одночасно, але не в цьому випадку, що я мав на увазі написати, що вони виконуються послідовно , перший awaitзачекає завершення першої функції повністю перед виконанням другого.
adeneo

3
@ Blindman67 - це однопоточна передача, але це обмеження не стосується методів асинхронізації, вони можуть працювати одночасно і повертати відповідь, коли вони виконані, тобто те, що ОП означає "паралель".
adeneo

7
@ Blindman67 - Я думаю, що цілком зрозуміло, що запитує ОП, використовуючи шаблон асинхрон / очікування, змусить функції працювати послідовно, навіть якщо вони є асинхронними, тому перша повністю закінчиться до виклику другого і т.д. ОП запитуючи, як викликати обидві функції в паралелі, і оскільки вони явно асинхронізовані, мета - запускати їх одночасно, тобто паралельно, наприклад, робити два запити ajax одночасно, що взагалі не є проблемою в JavaScript, як і більшість методів асинхронізації , як ви зазначали, запускає нативний код і використовує більше потоків.
adeneo

3
@Bergi це не дублікат зв'язаного питання - мова йде саме про асинхронний / очікуючий синтаксис та нативні Promises. Пов'язане питання стосується бібліотеки синіх птахів з генераторами та врожайністю. Концептуально подібний, можливо, але не в реалізації.
Iest

Відповіді:


700

Ви можете чекати на Promise.all():

await Promise.all([someCall(), anotherCall()]);

Щоб зберегти результати:

let [someResult, anotherResult] = await Promise.all([someCall(), anotherCall()]);

Зауважте, що це Promise.allне вдається швидко, це означає, що як тільки одне з обіцянок, яке йому надано, відхиляє, то вся справа відкидається.

const happy = (v, ms) => new Promise((resolve) => setTimeout(() => resolve(v), ms))
const sad = (v, ms) => new Promise((_, reject) => setTimeout(() => reject(v), ms))

Promise.all([happy('happy', 100), sad('sad', 50)])
  .then(console.log).catch(console.log) // 'sad'

Якщо ж замість цього ви хочете чекати, коли всі обіцянки будуть виконані або відхилені, тоді ви можете скористатися Promise.allSettled. Зауважте, що Internet Explorer не підтримує цей метод.

const happy = (v, ms) => new Promise((resolve) => setTimeout(() => resolve(v), ms))
const sad = (v, ms) => new Promise((_, reject) => setTimeout(() => reject(v), ms))

Promise.allSettled([happy('happy', 100), sad('sad', 50)])
  .then(console.log) // [{ "status":"fulfilled", "value":"happy" }, { "status":"rejected", "reason":"sad" }]


78
Очистіть, але пам'ятайте про швидку невдачу поведінки Promise.all. Якщо будь-яка з функцій видає помилку, Promise.all відхилить
NoNameProvided

11
Ви можете обробляти часткові результати добре з асинхронним / Await см stackoverflow.com/a/42158854/2019689
NoNameProvided

131
Підказка: використовуйте деструктуризацію масиву для ініціалізації довільної кількості результатів з Promise.all (), наприклад:[result1, result2] = Promise.all([async1(), async2()]);
jonny

10
@jonny Це предмет невдалий? Крім того, чи все-таки потрібно = await Promise.all?
ThetherSide

5
@theUtherSide Ви абсолютно праві - я знехтував включити очікування.
jonny

114

TL; DR

Використовуйте Promise.allдля викликів паралельних функцій, відповідь поводиться неправильно, коли виникає помилка.


Спочатку виконайте всі асинхронні дзвінки одразу та отримайте всі Promiseоб'єкти. По-друге, використання awaitна Promiseоб'єктах. Таким чином, поки ви чекаєте, коли перший Promiseвирішить інші асинхронні дзвінки, все ще прогресує. Загалом, ви будете чекати лише тих самих довгих, як найповільніший асинхронний дзвінок. Наприклад:

// Begin first call and store promise without waiting
const someResult = someCall();

// Begin second call and store promise without waiting
const anotherResult = anotherCall();

// Now we await for both results, whose async processes have already been started
const finalResult = [await someResult, await anotherResult];

// At this point all calls have been resolved
// Now when accessing someResult| anotherResult,
// you will have a value instead of a promise

Приклад JSbin: http://jsbin.com/xerifanima/edit?js,console

Caveat: Не має значення, чи є awaitдзвінки на одній лінії або на різних лініях, якщо перший awaitвиклик відбувається після всіх асинхронних дзвінків. Дивіться коментар JohnnyHK.


Оновлення: ця відповідь має різний час обробки помилок відповідно до відповіді @ bergi , вона НЕ викидає помилку під час виникнення помилки, але після виконання всіх обіцянок. Я порівнюю результат з порадою @ jonny:, [result1, result2] = Promise.all([async1(), async2()])перевірте наступний фрагмент коду

const correctAsync500ms = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 500, 'correct500msResult');
  });
};

const correctAsync100ms = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 100, 'correct100msResult');
  });
};

const rejectAsync100ms = () => {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 100, 'reject100msError');
  });
};

const asyncInArray = async (fun1, fun2) => {
  const label = 'test async functions in array';
  try {
    console.time(label);
    const p1 = fun1();
    const p2 = fun2();
    const result = [await p1, await p2];
    console.timeEnd(label);
  } catch (e) {
    console.error('error is', e);
    console.timeEnd(label);
  }
};

const asyncInPromiseAll = async (fun1, fun2) => {
  const label = 'test async functions with Promise.all';
  try {
    console.time(label);
    let [value1, value2] = await Promise.all([fun1(), fun2()]);
    console.timeEnd(label);
  } catch (e) {
    console.error('error is', e);
    console.timeEnd(label);
  }
};

(async () => {
  console.group('async functions without error');
  console.log('async functions without error: start')
  await asyncInArray(correctAsync500ms, correctAsync100ms);
  await asyncInPromiseAll(correctAsync500ms, correctAsync100ms);
  console.groupEnd();

  console.group('async functions with error');
  console.log('async functions with error: start')
  await asyncInArray(correctAsync500ms, rejectAsync100ms);
  await asyncInPromiseAll(correctAsync500ms, rejectAsync100ms);
  console.groupEnd();
})();


11
Це виглядає як набагато приємніший для мене варіант, ніж Promise.all - і з завданням руйнування ви навіть можете це зробити, [someResult, anotherResult] = [await someResult, await anotherResult]якщо ви перейдете constна let.
jawj

28
Але це все ще виконує awaitзаяви послідовно, правда? Тобто виконання пауз, поки перший не awaitвирішиться, потім переходить до другого. Promise.allвиконується паралельно.
Андру

8
Дякую @Haven Це має бути прийнятою відповіддю.
Стефан Д

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

15
@ Haven це рішення не те саме, що Promise.all. Якщо кожен запит - це мережевий дзвінок, await someResultйого потрібно буде вирішити, перш ніж await anotherResultйого навіть розпочати. І навпаки, у Promise.allдвох awaitдзвінках можна запуститись до того, як вирішено будь-який.
Бен Віндінг

89

Оновлення:

Оригінальна відповідь ускладнює (а в деяких випадках і неможливо) правильно поводитися з відмовами від обіцянок. Правильним рішенням є використання Promise.all:

const [someResult, anotherResult] = await Promise.all([someCall(), anotherCall()]);

Оригінальна відповідь:

Просто переконайтеся, що ви зателефонували до обох функцій, перш ніж чекати будь-яку:

// Call both functions
const somePromise = someCall();
const anotherPromise = anotherCall();

// Await both promises    
const someResult = await somePromise;
const anotherResult = await anotherPromise;

1
@JeffFischer Я додав коментарі, які, сподіваюся, роблять це зрозумілішим.
Джонатан Поттер

9
Я відчуваю, що це, безумовно, найчистіша відповідь
Гершом

1
Ця відповідь набагато зрозуміліша, ніж Гавена. Зрозуміло, що виклики функцій повертатимуть об'єкти обіцянки, а awaitпотім вирішать їх у фактичні значення.
user1032613,

3
Це, здається, працює при побіжному погляді, але має жахливі проблеми з неспроможними відхиленнями . Не використовуйте це!
Берги

1
@Bergi Ви маєте рацію, дякую, що вказали на це! Я оновив відповідь кращим рішенням.
Джонатан Поттер

24

Є ще один спосіб без Promise.all () зробити це паралельно:

По-перше, у нас є 2 функції для друку номерів:

function printNumber1() {
   return new Promise((resolve,reject) => {
      setTimeout(() => {
      console.log("Number1 is done");
      resolve(10);
      },1000);
   });
}

function printNumber2() {
   return new Promise((resolve,reject) => {
      setTimeout(() => {
      console.log("Number2 is done");
      resolve(20);
      },500);
   });
}

Це послідовно:

async function oneByOne() {
   const number1 = await printNumber1();
   const number2 = await printNumber2();
} 
//Output: Number1 is done, Number2 is done

Це паралельно:

async function inParallel() {
   const promise1 = printNumber1();
   const promise2 = printNumber2();
   const number1 = await promise1;
   const number2 = await promise2;
}
//Output: Number2 is done, Number1 is done

10

Це може бути досягнуто за допомогою Promise.allSettled () , яка схожа на, Promise.all()але без невдалої поведінки.

async function failure() {
    throw "Failure!";
}

async function success() {
    return "Success!";
}

const [failureResult, successResult] = await Promise.allSettled([failure(), success()]);

console.log(failureResult); // {status: "rejected", reason: "Failure!"}
console.log(successResult); // {status: "fulfilled", value: "Success!"}

Примітка . Це функція крайового кровотоку з обмеженою підтримкою веб-переглядача, тому я настійно рекомендую включити поліфункцію для цієї функції.


7

Я створив суть тестування деяких різних способів вирішення обіцянок з результатами. Можливо, буде корисно переглянути варіанти, які працюють.


Тести 4 і 6 в суті повернули очікувані результати. Див stackoverflow.com/a/42158854/5683904 по NoNameProvided , який пояснює різницю між варіантами.
akraines

1
    // A generic test function that can be configured 
    // with an arbitrary delay and to either resolve or reject
    const test = (delay, resolveSuccessfully) => new Promise((resolve, reject) => setTimeout(() => {
        console.log(`Done ${ delay }`);
        resolveSuccessfully ? resolve(`Resolved ${ delay }`) : reject(`Reject ${ delay }`)
    }, delay));

    // Our async handler function
    const handler = async () => {
        // Promise 1 runs first, but resolves last
        const p1 = test(10000, true);
        // Promise 2 run second, and also resolves
        const p2 = test(5000, true);
        // Promise 3 runs last, but completes first (with a rejection) 
        // Note the catch to trap the error immediately
        const p3 = test(1000, false).catch(e => console.log(e));
        // Await all in parallel
        const r = await Promise.all([p1, p2, p3]);
        // Display the results
        console.log(r);
    };

    // Run the handler
    handler();
    /*
    Done 1000
    Reject 1000
    Done 5000
    Done 10000
    */

Хоча налаштування p1, p2 та p3 не суворо виконує їх паралельно, вони не затримують жодного виконання, і ви можете зафіксувати контекстні помилки за допомогою лову.


2
Ласкаво просимо до переповнення стека. Хоча ваш код може дати відповідь на питання, будь ласка, додайте навколо нього контекст, щоб інші мали деяке уявлення про те, що він робить і чому він існує.
Тео

1

У моєму випадку у мене є кілька завдань, які я хочу виконати паралельно, але мені потрібно зробити щось інше з результатом цих завдань.

function wait(ms, data) {
    console.log('Starting task:', data, ms);
    return new Promise(resolve => setTimeout(resolve, ms, data));
}

var tasks = [
    async () => {
        var result = await wait(1000, 'moose');
        // do something with result
        console.log(result);
    },
    async () => {
        var result = await wait(500, 'taco');
        // do something with result
        console.log(result);
    },
    async () => {
        var result = await wait(5000, 'burp');
        // do something with result
        console.log(result);
    }
]

await Promise.all(tasks.map(p => p()));
console.log('done');

І вихід:

Starting task: moose 1000
Starting task: taco 500
Starting task: burp 5000
taco
moose
burp
done

круто для динамічного створення (масив ресурсів)
Michal Miky Jankovský

1

чекати Promise.all ([деякийCall (), іншийCall ()]); як уже згадується, він буде виконувати функцію забору з ниток (дуже поширений паралельний код як CUDA), отже, він дозволить виконувати всі обіцянки в ньому, не блокуючи один одного, але заважатиме виконанню продовжуватися, поки ВСІ не будуть вирішені.

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

// create a queue object with concurrency 2
var q = async.queue(function(task, callback) {
  console.log('Hello ' + task.name);
  callback();
}, 2);

// assign a callback
q.drain = function() {
  console.log('All items have been processed');
};

// add some items to the queue
q.push({name: 'foo'}, function(err) {
  console.log('Finished processing foo');
});

q.push({name: 'bar'}, function (err) {
  console.log('Finished processing bar');
});

// add some items to the queue (batch-wise)
q.push([{name: 'baz'},{name: 'bay'},{name: 'bax'}], function(err) {
  console.log('Finished processing item');
});

// add some items to the front of the queue
q.unshift({name: 'bar'}, function (err) {
  console.log('Finished processing bar');
});

Подяки автору статті статті ( читати далі )


-5

Я голосую за:

await Promise.all([someCall(), anotherCall()]);

Будьте в курсі того моменту, коли ви викликаєте функції, це може спричинити несподіваний результат:

// Supposing anotherCall() will trigger a request to create a new User

if (callFirst) {
  await someCall();
} else {
  await Promise.all([someCall(), anotherCall()]); // --> create new User here
}

Але наступне завжди запускає запит на створення нового Користувача

// Supposing anotherCall() will trigger a request to create a new User

const someResult = someCall();
const anotherResult = anotherCall(); // ->> This always creates new User

if (callFirst) {
  await someCall();
} else {
  const finalResult = [await someResult, await anotherResult]
}

Оскільки ви оголосили функцію за межами / перед випробуванням стану, і викликали їх. Спробуйте загорнути їх у elseблок.
Haven

@Haven: Я маю на увазі, коли ви розділяєте моменти, коли ви викликаєте функції від очікування, це може призвести до несподіваних результатів, наприклад: async HTTP-запитів.
Hoang Le Anh Tu

-6

Я створюю функцію помічника waitAll, можливо, це може зробити її солодшою. Наразі він працює лише у nodejs , а не в chrome browser.

    //const parallel = async (...items) => {
    const waitAll = async (...items) => {
        //this function does start execution the functions
        //the execution has been started before running this code here
        //instead it collects of the result of execution of the functions

        const temp = [];
        for (const item of items) {
            //this is not
            //temp.push(await item())
            //it does wait for the result in series (not in parallel), but
            //it doesn't affect the parallel execution of those functions
            //because they haven started earlier
            temp.push(await item);
        }
        return temp;
    };

    //the async functions are executed in parallel before passed
    //in the waitAll function

    //const finalResult = await waitAll(someResult(), anotherResult());
    //const finalResult = await parallel(someResult(), anotherResult());
    //or
    const [result1, result2] = await waitAll(someResult(), anotherResult());
    //const [result1, result2] = await parallel(someResult(), anotherResult());

3
Ні, паралелізація тут взагалі не відбувається. forЦикл послідовно чекає кожну обіцянку і додає результат в масив.
Szczepan Hołyszewski

Я розумію, це, здається, не працює для людей. Тож я перевірив у node.js та браузері. Тест проходить у node.js (v10, v11), firefox, він не працює в браузері chrome. Тестовий випадок знаходиться в gist.github.com/fredyang/ea736a7b8293edf7a1a25c39c7d2fbbf
Фред Ян

2
Я відмовляюся вірити в це. У стандарті немає нічого, що говорить, що різні ітерації циклу for можуть бути автоматично паралелізовані; це не так, як працює JavaScript. Спосіб написання коду циклу означає це: "чекайте на один елемент (очікуваний expr), потім натискайте результат на темп, потім приймайте наступний елемент (наступна ітерація циклу for)." Очікування "для кожного елемента повністю Якщо тести показують, що існує паралелізація, це повинно бути, тому що транспілятор робить щось нестандартне або
вивертає

@ SzczepanHołyszewski Ваша впевненість у забороні, не запускаючи тестовий випадок, надихає мене зробити кілька перейменованих рефакторних та додаткових коментарів. Весь код є звичайним старим ES6, не потрібно проводити трансляцію.
Фред Ян

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