Вызов асинхронных / ожидающих функций параллельно

434

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

await someCall();
await anotherCall();

Правильно ли я понимаю, что anotherCall()будет вызываться только после someCall()завершения? Каков самый элегантный способ их параллельного вызова?

Я хочу использовать его в Node, так что, возможно, есть решение с асинхронной библиотекой?

РЕДАКТИРОВАТЬ: Я не удовлетворен решением, предоставленным в этом вопросе: замедление из-за непараллельного ожидания обещаний в асинхронных генераторах , потому что он использует генераторы, и я спрашиваю о более общем случае использования.

Виктор Марчук
источник
1
@adeneo Это неверно, Javascript никогда не работает параллельно в своем собственном контексте.
Blindman67
5
@ Blindman67 - он делает, по крайней мере, так, как означает OP, когда две асинхронные операции выполняются одновременно, но не в этом случае я хотел написать, что они выполняются последовательно , первая awaitбудет ждать завершения первой функции полностью перед выполнением второго.
Adeneo
3
@ Blindman67 - это однопоточный, но это ограничение не распространяется на асинхронные методы, они могут запускаться одновременно и возвращать ответ, когда они сделаны, то есть то, что OP означает «параллелизм».
Adeneo
7
@ Blindman67 - Я думаю, что довольно ясно, о чем спрашивает OP, использование шаблона async / await заставит функции работать последовательно, даже если они асинхронные, поэтому первая полностью завершится до вызова второй и т. Д. OP спрашивая, как вызывать обе функции в параллели, и, поскольку они явно асинхронны, цель состоит в том, чтобы запустить их одновременно, то есть в параллели, например, выполняя два ajax-запроса одновременно, что совсем не проблема в javascript, как большинство асинхронных методов , как вы заметили, запускает собственный код и использует больше потоков.
Adeneo
3
@Bergi это не дубликат связанного вопроса - речь идет конкретно о синтаксисе async / await и нативных Promises. Связанный вопрос касается библиотеки bluebird с генераторами и yield. Концептуально подобное возможно, но не в реализации.
IEST

Ответы:

703

Вы можете ждать на 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" }]

madox2
источник
79
Очистите, но помните о быстром сбое поведения Promise.all. Если какая-либо из функций выдаст ошибку, Promise.all будет отклонен
NoNameProvided
11
Вы можете обрабатывать частичные результаты хорошо с асинхронным / Await см stackoverflow.com/a/42158854/2019689
NoNameProvided
131
Совет для профессионалов: используйте деструктуризацию массива, чтобы инициализировать произвольное количество результатов из Promise.all (), например:[result1, result2] = Promise.all([async1(), async2()]);
Джонни,
10
@jonny Является ли это быстро проваливается? Кроме того, нужно = await Promise.allли это еще ?
TheStherSide
5
@theUtherSide Вы абсолютно правы - я забыл включить ожидание.
Джонни
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

Предостережение: не имеет значения, 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.
Джо
28
Но это все еще выполняет awaitоператоры последовательно, верно? То есть выполнение приостанавливается до тех пор, пока первое не awaitразрешится, затем перейдет ко второму. Promise.allвыполняется параллельно.
Andru
8
Спасибо @Haven. Это должен быть принятый ответ.
Стефан Д.
87
Этот ответ вводит в заблуждение, поскольку тот факт, что оба ожидания выполняются в одной строке, не имеет значения. Важно то, что два асинхронных вызова выполняются до того, как ожидается какой-либо из них.
JohnnyHK
15
@ Небеса это решение не то же самое, что 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
@ Берги Вы правы, спасибо за указание на это! Я обновил ответ с лучшим решением.
Джонатан Поттер
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
user2883596
источник
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

Я создал суть, тестирующую несколько разных способов выполнения обещаний, с результатами. Может быть полезно увидеть варианты, которые работают.

SkarXa
источник
Испытания 4 и 6 в сущности дали ожидаемые результаты. См stackoverflow.com/a/42158854/5683904 по NoNameProvided , который объясняет разницу между вариантами.
Украина
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 не выполняет их строго параллельно, они не задерживают выполнение, и вы можете перехватывать контекстные ошибки с помощью перехвата.

Thrunobulax
источник
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
Алекс Дреско
источник
круто для динамического создания (множество ресурсов)
Михал Мики Янковский
1

ожидайте Promise.all ([someCall (), anotherCall ()]); как уже упоминалось, будет действовать как забор потока (очень распространенный в параллельном коде, как CUDA), следовательно, он позволит выполнять все обещания в нем, не блокируя друг друга, но будет препятствовать продолжению выполнения до разрешения ALL.

Другим подходом, которым стоит поделиться, является асинхронность Node.js, которая также позволит вам легко контролировать уровень параллелизма, который обычно желателен, если задача напрямую связана с использованием ограниченных ресурсов в качестве вызова 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');
});

Кредиты автору статьи Medium ( подробнее )

Тьяго Конрадо
источник
-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: я имею в виду, что когда вы отделяете моменты, когда вы вызываете функции, а не ожидайте, это может привести к неожиданным результатам, например: асинхронные HTTP-запросы.
Хоанг Ле Ан Ту
-6

Я создаю вспомогательную функцию waitAll, может быть, она может сделать ее слаще. Пока он работает только в nodejs , а не в браузере Chrome.

    //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Цикл последовательно ждет каждое обещание и добавляет результат в массив.
Щепан Холышевский
Я понимаю, что это не работает для людей. Итак, я тестировал в node.js и браузере. Тест проходит в node.js (v10, v11), Firefox, он не работает в браузере Chrome. Контрольный пример находится в gist.github.com/fredyang/ea736a7b8293edf7a1a25c39c7d2fbbf
Фред Ян
2
Я отказываюсь верить в это. В стандарте нет ничего, что говорило бы, что разные итерации цикла for могут быть автоматически распараллелены; это не то, как работает JavaScript. То, как написан код цикла, означает следующее: «ожидайте один элемент (выражение await), ТО затем подталкивайте результат к температуре, ТО принимайте следующий элемент (следующая итерация цикла for).« Ожидание »для каждого элемента полностью ограничивается одной итерацией цикла. Если тесты показывают, что есть распараллеливание, это должно быть из-за того, что транспортер делает что-то нестандартное или просто
глючит
@ SzczepanHołyszewski Ваша уверенность в том, что вы не можете выполнить тестовый сценарий, вдохновляет меня на некоторые исправления переименования и дополнительные комментарии. Весь код - старый ES6, не требуется транспиляция.
Фред Ян
Не уверен, почему это так сильно отрицательно. По сути, это тот же ответ, который дал @ user2883596.
Джонатан Судиаман