development

Await Promise.all ()과 다중 대기의 차이점은 무엇입니까?

big-blog 2020. 7. 26. 11:37
반응형

Await Promise.all ()과 다중 대기의 차이점은 무엇입니까?


다음과 같은 차이점이 있습니까?

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];

참고 :

이 답변 await은 시리즈와 사이의 타이밍 차이 만 다룹니다 Promise.all. 오류 처리의 더 중요한 차이점을 다루는 @mikep의 포괄적 인 답변 을 읽으십시오 .


이 답변의 목적을 위해 몇 가지 예제 방법을 사용합니다.

  • res(ms) 은 밀리 초의 정수를 취하고 그 밀리 초 후에 해결되는 약속을 반환하는 함수입니다.
  • 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.all3 초 후에 내부 약속의 데이터로 해결됨을 의미합니다 .

그러나 Promise.all"실패"동작이 있습니다 .

실시 예 # 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대신 사용 하는 경우 각 약속이 순차적으로 해결 될 때까지 기다려야합니다.

실시 예 # 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()


첫 번째 차이점-빨리 실패

I agree with @zzzzBov's answer but "fail fast" advantage of Promise.all is not only the one difference. Some users in comments asks why to use Promise.all when it is only faster in negative scenario (when some task fails). And I ask why not? If I have two independent async parallel tasks and first one is resolved in very long time but second is rejected in very short time why to leave user wait for error mesage "very long time" instead of "very short time"? In real-life applications we must consider negative scenario. But OK - in this first difference you can decide which alternative to use Promise.all vs. multiple await.

Second difference - error handling

But when considering error handling YOU MUST use Promise.all. It is not possible to correctly handle errors of async parallel tasks triggered with multiple await. In negative scenario you will always end with UnhandledPromiseRejectionWarning and PromiseRejectionHandledWarning although you use try/catch anywhere. That is why Promise.all was designed. Of course someone could say that we can suppress that errors using process.on('unhandledRejection', err => {}) and process.on('rejectionHandled', err => {}) but it is not good practice. I found many examples on the internet that does not consider error handling for two or more independent async parallel tasks at all or consider it but in wrong way - just using try/catch and hoping it will catch errors. It's almost impossible to find good practice. That is why I'm writing this answer.

Summary

Never use multiple await for two or more independent async parallel tasks because of you will not be able to handle errors seriously. Always use Promise.all() for this use case. Async/await is not replacement for Promises. It is just pretty way how to use promises... async code is written in sync style and we can avoid multiple then in promises.

Some people say that using Promise.all() we can not handle tasks errors separately but only error from first rejected promise (yes, some use cases may require separate handling e.g. for logging). It is not problem - see "Addition" heading below.

Examples

Consider this async task...

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

When you run tasks in positive scenario there is no difference between Promise.all and multiple await. Both examples end with Task 1 succeed! Task 2 succeed! after 5 seconds.

// 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!

When first task takes 10 seconds in positive scenario and seconds task takes 5 seconds in negative scenario there are differences in errors issued.

// 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!

We should already notice here that we are doing something wrong when using multiple await in parallel. Of course to avoid errors we should handle it! Let's try...


// 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!

As you can see to successfully handle error we need to add just one catch to run function and code with catch logic is in callback (async style). We do not need handle errors inside run function because async function it does automatically - promise rejection of task function causes rejection of run function. To avoid callback we can use sync style (async/await + try/catch) try { await run(); } catch(err) { } but in this example it is not possible because we can not use await in main thread - it can be used only in async function (it is logical because nobody wants to block main thread). To test if handling works in sync style we can call run function from another async function or use IIFE (Immediately Invoked Function Expression): (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();.

This is only one correct way how to run two or more async parallel tasks and handle errors. You should avoid examples below.


// 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);
};

We can try to handle code above several ways...

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 

... nothing got caught because it handles sync code but run is async

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)

... Wtf? We see firstly that error for task 2 was not handled and later that was caught. Misleading and still full of errors in console. Unusable this way.

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

... the same as above. User @Qwerty in his deleted answer asked about this strange behavior that seems to be catched but there are also unhandled errors. We catch error because run() is rejected on line with await keyword and can be catched using try/catch when calling run(). We also get unhandled error because we are calling async task function synchronously (without await keyword) and this task runs outside run() function and also fails outside. It is similar when we are not able to handle error by try/catch when calling some sync function which part of code runs in setTimeout... 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)

... "only" two errors (3rd one is missing) but nothing caught.


Addition (handle task errors separately and also first-fail error)

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!

... note that in this example I used negativeScenario=true for both tasks for better demonstration what happens (throw err is used to fire final error)


You can check for yourself.

In this fiddle, I ran a test to demonstrate the blocking nature of await, as opposed to Promise.all which will start all of the promises and while one is waiting it will go on with the others.


In case of await Promise.all([task1(), task2()]); "task1()" and "task2()" will run parallel and will wait until both promises are completed (either resolved or rejected). Whereas in case of

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

t2 will only run after t1 has finished execution (has been resolved or rejected). Both t1 and t2 will not run parallel.

참고URL : https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await

반응형