await Promise.all() 和多个 await 之间有啥区别?

Posted

技术标签:

【中文标题】await Promise.all() 和多个 await 之间有啥区别?【英文标题】:Any difference between await Promise.all() and multiple await?await Promise.all() 和多个 await 之间有什么区别? 【发布时间】:2017-12-30 07:22:46 【问题描述】:

两者有什么区别:

const [result1, result2] = await Promise.all([task1(), task2()]);

const t1 = task1();
const t2 = task2();

const result1 = await t1;
const result2 = await t2;

const [t1, t2] = [task1(), task2()];
const [result1, result2] = [await t1, await t2];

【问题讨论】:

密切相关:Waiting for more than one concurrent await operation 这个问题对我来说是金。在这里学到了很多 【参考方案1】:

注意

此答案仅涵盖串联awaitPromise.all 之间的时间差异。请务必阅读@mikep's comprehensive answer that also covers the more important differences in error handling。


为了回答这个问题,我将使用一些示例方法:

res(ms) 是一个函数,它需要一个整数毫秒,并返回一个在该毫秒数后解析的 Promise。 rej(ms) 是一个函数,它需要一个整数毫秒并返回一个承诺,该承诺会在那么多毫秒后被拒绝。

调用res 会启动计时器。使用Promise.all 等待一些延迟将在所有延迟完成后解决,但请记住它们同时执行:

示例#1
const data = await Promise.all([res(3000), res(2000), res(1000)])
//                              ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^
//                               delay 1    delay 2    delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========O                     delay 3
//
// =============================O Promise.all

async function example() 
  const start = Date.now()
  let i = 0
  function res(n) 
    const id = ++i
    return new Promise((resolve, reject) => 
      setTimeout(() => 
        resolve()
        console.log(`res #$id called after $n milliseconds`, Date.now() - start)
      , n)
    )
  

  const data = await Promise.all([res(3000), res(2000), res(1000)])
  console.log(`Promise.all finished`, Date.now() - start)


example()

这意味着Promise.all 将在 3 秒后使用来自内部 Promise 的数据进行解析。

但是,Promise.all has a "fail fast" behavior:

示例#2
const data = await Promise.all([res(3000), res(2000), rej(1000)])
//                              ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^
//                               delay 1    delay 2    delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========X                     delay 3
//
// =========X                     Promise.all

async function example() 
  const start = Date.now()
  let i = 0
  function res(n) 
    const id = ++i
    return new Promise((resolve, reject) => 
      setTimeout(() => 
        resolve()
        console.log(`res #$id called after $n milliseconds`, Date.now() - start)
      , n)
    )
  
  
  function rej(n) 
    const id = ++i
    return new Promise((resolve, reject) => 
      setTimeout(() => 
        reject()
        console.log(`rej #$id called after $n milliseconds`, Date.now() - start)
      , n)
    )
  
  
  try 
    const data = await Promise.all([res(3000), res(2000), rej(1000)])
   catch (error) 
    console.log(`Promise.all finished`, Date.now() - start)
  


example()

如果你改用async-await,你将不得不等待每个promise依次解析,这可能效率不高:

示例#3
const delay1 = res(3000)
const delay2 = res(2000)
const delay3 = rej(1000)

const data1 = await delay1
const data2 = await delay2
const data3 = await delay3

// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========X                     delay 3
//
// =============================X await

async function example() 
  const start = Date.now()
  let i = 0
  function res(n) 
    const id = ++i
    return new Promise((resolve, reject) => 
      setTimeout(() => 
        resolve()
        console.log(`res #$id called after $n milliseconds`, Date.now() - start)
      , n)
    )
  
  
  function rej(n) 
    const id = ++i
    return new Promise((resolve, reject) => 
      setTimeout(() => 
        reject()
        console.log(`rej #$id called after $n milliseconds`, Date.now() - start)
      , n)
    )
  
  
  try 
    const delay1 = res(3000)
    const delay2 = res(2000)
    const delay3 = rej(1000)

    const data1 = await delay1
    const data2 = await delay2
    const data3 = await delay3
   catch (error) 
    console.log(`await finished`, Date.now() - start)
  


example()

【讨论】:

所以基本上区别只是 Promise.all 的“快速失败”功能? 所以关于一切都成功的成功案例,没有任何情况Promise.all()会比背靠背做3个await更快,对吗?所以出于性能原因做Promise.all() 是没有用的,除非可能是为了快速失败的场景? @HenriLapierre,我见过太多开发人员错误地执行串行awaits(即data1 = await thing1(); data2 = await thing2(); data3 = await thing3();),以为他们正在并行运行承诺。因此,为了回答您的问题,如果您的承诺已经开始,则无法更快地解决问题。我不知道你为什么会认为他们可以通过Promise.all() 以某种方式加速。 @mclzc 在示例 #3 中,进一步的代码执行被暂停,直到 delay1 解决。甚至在“如果你使用 async-await 代替,你将不得不等待每个 Promise 依次解决”的文本中。 它可能效率不高” - 更重要的是,导致unhandledrejection 错误。你永远不会想要使用它。请将此添加到您的答案中。【参考方案2】:

第一个区别 - 快速失败

我同意@zzzzBov 的回答,但Promise.all 的“快速失败”优势并不是唯一的区别。 cmets 中的一些用户问为什么使用Promise.all 是值得的,因为它只在负面情况下更快(当某些任务失败时)。我问,为什么不呢?如果我有两个独立的异步并行任务,第一个需要很长时间才能解决,但第二个在很短的时间内被拒绝,为什么要让用户等待更长的调用完成才能收到错误消息?在实际应用中,我们必须考虑负面情况。但是好的 - 在第一个区别中,您可以决定使用哪个替代方案:Promise.all 与多个 await

第二个区别 - 错误处理

但在考虑错误处理时,您必须使用Promise.all。无法正确处理由多个awaits 触发的异步并行任务的错误。在负面情况下,无论您在哪里使用 try/catch,您都将始终以 UnhandledPromiseRejectionWarningPromiseRejectionHandledWarning 结尾。这就是设计Promise.all 的原因。当然有人可能会说我们可以使用process.on('unhandledRejection', err => )process.on('rejectionHandled', err => ) 来抑制这些错误,但这不是一个好习惯。我在互联网上发现了许多根本不考虑对两个或多个独立异步并行任务进行错误处理的示例,或者以错误的方式考虑它 - 只是使用 try/catch 并希望它会捕获错误。在这方面几乎不可能找到好的做法。

总结

TL;DR:切勿将多个await 用于两个或多个独立的异步并行任务,因为您将无法正确处理错误。对于此用例,请始终使用 Promise.all()

Async/ await 不是 Promises 的替代品,它只是一种使用 Promise 的漂亮方式。异步代码是用“同步风格”编写的,我们可以避免在 Promise 中出现多个 thens。

有人说,当使用Promise.all() 时,我们不能单独处理任务错误,我们只能处理来自第一个被拒绝的promise 的错误(单独处理可能很有用,例如用于日志记录)。这不是问题 - 请参阅此答案底部的“添加”标题。

示例

考虑这个异步任务...

const task = function(taskNum, seconds, negativeScenario) 
  return new Promise((resolve, reject) => 
    setTimeout(_ => 
      if (negativeScenario)
        reject(new Error('Task ' + taskNum + ' failed!'));
      else
        resolve('Task ' + taskNum + ' succeed!');
    , seconds * 1000)
  );
;

当您在正面场景中运行任务时,Promise.all 和多个awaits 之间没有区别。两个示例都在 5 秒后以 Task 1 succeed! Task 2 succeed! 结尾。

// Promise.all alternative
const run = async function() 
  // tasks run immediate in parallel and wait for both results
  let [r1, r2] = await Promise.all([
    task(1, 5, false),
    task(2, 5, false)
  ]);
  console.log(r1 + ' ' + r2);
;
run();
// at 5th sec: Task 1 succeed! Task 2 succeed!
// multiple await alternative
const run = async function() 
  // tasks run immediate in parallel
  let t1 = task(1, 5, false);
  let t2 = task(2, 5, false);
  // wait for both results
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
;
run();
// at 5th sec: Task 1 succeed! Task 2 succeed!

但是,当第一个任务耗时 10 秒成功,第二个任务耗时 5 秒但失败时,发出的错误存在差异。

// Promise.all alternative
const run = async function() 
  let [r1, r2] = await Promise.all([
      task(1, 10, false),
      task(2, 5, true)
  ]);
  console.log(r1 + ' ' + r2);
;
run();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// multiple await alternative
const run = async function() 
  let t1 = task(1, 10, false);
  let t2 = task(2, 5, true);
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
;
run();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
// at 10th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!

我们应该已经注意到,在并行使用多个awaits 时我们做错了。让我们尝试处理错误:

// Promise.all alternative
const run = async function() 
  let [r1, r2] = await Promise.all([
    task(1, 10, false),
    task(2, 5, true)
  ]);
  console.log(r1 + ' ' + r2);
;
run().catch(err =>  console.log('Caught error', err); );
// at 5th sec: Caught error Error: Task 2 failed!

如您所见,要成功处理错误,我们只需向run 函数添加一个catch,并将带有catch 逻辑的代码添加到回调中。我们不需要处理 run 函数内部的错误,因为异步函数会自动执行此操作 - 承诺拒绝 task 函数会导致拒绝 run 函数。

为了避免回调,我们可以使用“同步样式”(async/await + try/catch)try await run(); catch(err) 但是在这个例子中这是不可能的,因为我们不能在主线程中使用await——它只能在异步函数中使用(因为没有人想阻塞主线程)。要测试处理是否以“同步风格”工作,我们可以从另一个异步函数调用 run 函数或使用 IIFE(立即调用函数表达式:MDN):

(async function()  
  try  
    await run(); 
   catch(err)  
    console.log('Caught error', err); 
  
)();

这是运行两个或多个异步并行任务并处理错误的唯一正确方法。您应该避免使用以下示例。

不好的例子

// multiple await alternative
const run = async function() 
  let t1 = task(1, 10, false);
  let t2 = task(2, 5, true);
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
;

我们可以尝试通过几种方式处理上述代码中的错误...

try  run();  catch(err)  console.log('Caught error', err); ;
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled 

...没有任何东西被捕获,因为它处理同步代码,但 run 是异步的。

run().catch(err =>  console.log('Caught error', err); );
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: Caught error Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

……嗯?我们首先看到任务 2 的错误没有被处理,后来它被捕获。在控制台中误导并且仍然充满错误,它仍然无法使用这种方式。

(async function()  try  await run();  catch(err)  console.log('Caught error', err); ; )();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: Caught error Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

...同上。用户@Qwerty 在他删除的答案中询问了这种奇怪的行为,其中错误似乎被捕获但也未处理。我们会捕获错误,因为run() 在使用await 关键字的行被拒绝,并且可以在调用run() 时使用try/catch 捕获。我们还会收到 unhandled 错误,因为我们正在同步调用异步任务函数(没有 await 关键字),并且此任务在 run() 函数之外运行并失败。 这类似于我们在调用某些调用 setTimeout 的同步函数时无法通过 try/catch 处理错误:

function test() 
  setTimeout(function()  
    console.log(causesError); 
    , 0);
; 
try  
  test(); 
 catch(e)  
  /* this will never catch error */ 
`.

另一个糟糕的例子:

const run = async function() 
  try 
    let t1 = task(1, 10, false);
    let t2 = task(2, 5, true);
    let r1 = await t1;
    let r2 = await t2;
  
  catch (err) 
    return new Error(err);
  
  console.log(r1 + ' ' + r2);
;
run().catch(err =>  console.log('Caught error', err); );
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

...“只有”两个错误(缺少第 3 个)但没有发现任何错误。

加法(处理单独的任务错误和首次失败错误)

const run = async function() 
  let [r1, r2] = await Promise.all([
    task(1, 10, true).catch(err =>  console.log('Task 1 failed!'); throw err; ),
    task(2, 5, true).catch(err =>  console.log('Task 2 failed!'); throw err; )
  ]);
  console.log(r1 + ' ' + r2);
;
run().catch(err =>  console.log('Run failed (does not matter which task)!'); );
// at 5th sec: Task 2 failed!
// at 5th sec: Run failed (does not matter which task)!
// at 10th sec: Task 1 failed!

...请注意,在此示例中,我拒绝了这两个任务以更好地演示发生了什么(throw err 用于触发最终错误)。

【讨论】:

这个答案比接受的答案更好,因为当前接受的答案错过了非常重要的错误处理主题 线程就不用说了,都是在一个线程中运行的。 Concurrency is not Parallelism【参考方案3】:

通常,使用Promise.all() 会并行运行“异步”请求。使用await 可以并行运行被“同步”阻塞。

下面的

test1test2 函数展示了 await 如何运行异步或同步。

test3 显示 Promise.all() 是异步的。

jsfiddle with timed results - 打开浏览器控制台查看测试结果

同步行为。不并行运行,需要 ~1800ms

const test1 = async () => 
  const delay1 = await Promise.delay(600); //runs 1st
  const delay2 = await Promise.delay(600); //waits 600 for delay1 to run
  const delay3 = await Promise.delay(600); //waits 600 more for delay2 to run
;

异步行为。并行运行,耗时 ~600ms

const test2 = async () => 
  const delay1 = Promise.delay(600);
  const delay2 = Promise.delay(600);
  const delay3 = Promise.delay(600);
  const data1 = await delay1;
  const data2 = await delay2;
  const data3 = await delay3; //runs all delays simultaneously

异步行为。并行运行,耗时 ~600ms

const test3 = async () => 
  await Promise.all([
  Promise.delay(600), 
  Promise.delay(600), 
  Promise.delay(600)]); //runs all delays simultaneously
;

TLDR;如果您使用Promise.all(),它也会“快速失败” - 在任何包含的函数第一次失败时停止运行。

【讨论】:

我在哪里可以获得关于 sn-ps 1 和 2 中幕后发生的事情的详细说明?我很惊讶它们有不同的运行方式,因为我期望行为是相同的。 @Gregordy 是的,这令人惊讶。我发布了这个答案,以节省新来的编码员异步一些头疼的问题。这一切都与 JS 何时评估等待有关,这就是为什么分配变量的方式很重要。深度异步阅读:blog.bitsrc.io/… 在 sn-p 1 中,您可以使用 try..catch 优雅地处理错误。在 sn-p 2 中,您遇到了@mikep 提到的未处理的承诺拒绝问题 如果你只有少数异步函数或者它们非常快,那么完成时间的差异可以忽略不计。但是如果你有数百个,不使用 Promise.all 会浪费大量资源,因为事件循环会按顺序处理它们(除非你需要它们)。尝试在 async/await 中构建一个网络爬虫,你会发现问题【参考方案4】:

您可以自己检查。

在这个fiddle 中,我运行了一个测试来证明await 的阻塞性质,而不是Promise.all,它会启动所有的promise,当一个人在等待时它会继续其他的。

【讨论】:

实际上,您的小提琴并没有解决他的问题。之后调用t1 = task1(); t2 = task2()then 对他们俩都使用await 就像在他的问题中一样result1 = await t1; result2 = await t2; 与您正在测试的使用await 不同原始电话如result1 = await task1(); result2 = await task2();。他问题中的代码确实立即启动了所有承诺。正如答案所示,不同之处在于使用Promise.all 方式会更快地报告失败。 您的回答与@BryanGrezeszak 评论的主题无关。您应该删除它以避免误导用户。 离题了。但这可能有助于某人更好地理解,它帮助了我【参考方案5】:

await Promise.all([task1(), task2()]); 的情况下,“task1()”和“task2()”将并行运行并等待两个promise都完成完成(解决或拒绝)。而在

的情况下
const result1 = await t1;
const result2 = await t2;

t2 只会在 t1 完成执行(已解决或拒绝)后运行。 t1 和 t2 都不会并行运行。

【讨论】:

以上是关于await Promise.all() 和多个 await 之间有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

await 与 Promise.all 结合使用

如何用 Promise.all 替换多个 async/await 调用?

async-await 同时触发(等待)多个异步操作

promise,async await,try catch的问题整理

async/await实现Promise.all()

promiseall退出条件