Jakaś różnica między await Promise.all () a multiple await?

181

Czy jest jakaś różnica między:

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

i

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

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

i

const [t1, t2] = [task1(), task2()];
const [result1, result2] = [await t1, await t2];
Ukryty
źródło

Odpowiedzi:

210

Uwaga :

Ta odpowiedź obejmuje tylko różnice czasowe między awaitseriami a Promise.all. Koniecznie przeczytaj wyczerpującą odpowiedź @ mikep, która obejmuje również ważniejsze różnice w obsłudze błędów .


Na potrzeby tej odpowiedzi posłużę się kilkoma przykładowymi metodami:

  • res(ms) jest funkcją, która przyjmuje liczbę całkowitą z milisekund i zwraca obietnicę, która jest rozpatrywana po tych wielu milisekundach.
  • rej(ms) jest funkcją, która przyjmuje liczbę całkowitą z milisekund i zwraca obietnicę, która odrzuca po tych wielu milisekundach.

Połączenie resuruchamia stoper. Korzystanie Promise.allz czekania na kilka opóźnień ustąpi po zakończeniu wszystkich opóźnień, ale pamiętaj, że są one wykonywane w tym samym czasie:

Przykład 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

Oznacza to, że Promise.allproblem zostanie rozwiązany z danymi z wewnętrznych obietnic po 3 sekundach.

Ale Promise.allma zachowanie „szybkie niepowodzenie” :

Przykład nr 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

Jeśli async-awaitzamiast tego użyjesz , będziesz musiał poczekać, aż każda obietnica zostanie rozwiązana sekwencyjnie, co może nie być tak wydajne:

Przykład nr 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

zzzzBov
źródło
4
Więc zasadniczo różnica polega po prostu na „szybkiej awarii” Promise.all?
Matthew
4
@mclzc W przykładzie nr 3 dalsze wykonywanie kodu jest wstrzymywane do czasu rozwiązania opóźnienia1. Jest to nawet w tekście „Jeśli zamiast tego użyjesz async-await, będziesz musiał poczekać, aż każda obietnica zostanie rozwiązana po kolei”
haggis,
1
@Qback, istnieje aktywny fragment kodu, który demonstruje to zachowanie. Rozważ uruchomienie go i ponowne odczytanie kodu. Nie jesteś pierwszą osobą, która źle zrozumiała, jak zachowuje się sekwencja obietnic. Błąd, który popełniłeś w swoim demo, polega na tym, że nie zaczynasz składać obietnic w tym samym czasie.
zzzzBov
1
@zzzzBov Masz rację. Zaczynasz to w tym samym czasie. Przepraszam, przyszedłem do tego pytania z innego powodu i to przeoczyłem.
Qback
2
może nie być tak wydajne ” - a co ważniejsze, powodować unhandledrejectionbłędy. Nigdy nie będziesz chciał tego używać. Dodaj to do swojej odpowiedzi.
Bergi
88

Pierwsza różnica - porażka szybko

Zgadzam się z odpowiedzią @ zzzzBov, ale przewaga Promise.all „szybko zawiedzie” to nie tylko jedna różnica. Niektórzy użytkownicy w komentarzach pytają, dlaczego używać Promise.all, gdy jest to szybsze tylko w negatywnym scenariuszu (gdy niektóre zadanie nie powiedzie się). I pytam dlaczego nie? Jeśli mam dwa niezależne, równoległe zadania asynchroniczne, a pierwsze z nich jest rozwiązywane w bardzo długim czasie, ale drugie jest odrzucane w bardzo krótkim czasie, dlaczego użytkownik ma czekać na komunikat o błędzie „bardzo długi czas” zamiast „bardzo krótki czas”? W rzeczywistych zastosowaniach musimy wziąć pod uwagę negatywny scenariusz. Ale OK - w tej pierwszej różnicy możesz zdecydować, która alternatywa dla Promise.all czy wielokrotnego czekania.

Druga różnica - obsługa błędów

Ale rozważając obsługę błędów, MUSISZ użyć Promise.all. Nie jest możliwe poprawne obsługiwanie błędów asynchronicznych zadań równoległych wyzwalanych z wieloma oczekiwaniami. W negatywnym scenariuszu zawsze skończysz UnhandledPromiseRejectionWarningi PromiseRejectionHandledWarningchociaż używasz try / catch wszędzie. Dlatego powstał Promise.all. Oczywiście ktoś mógłby powiedzieć, że możemy stłumić te błędy używając process.on('unhandledRejection', err => {})i process.on('rejectionHandled', err => {})ale to nie jest dobra praktyka. Znalazłem wiele przykładów w Internecie, które nie uwzględniają obsługi błędów dla dwóch lub więcej niezależnych równoległych zadań asynchronicznych w ogóle lub rozważają to w niewłaściwy sposób - po prostu używając try / catch i mając nadzieję, że wykryje błędy. Znalezienie dobrej praktyki jest prawie niemożliwe. Dlatego piszę tę odpowiedź.

Podsumowanie

Nigdy nie używaj wielokrotnego oczekiwania dla dwóch lub więcej niezależnych równoległych zadań asynchronicznych, ponieważ nie będziesz w stanie poważnie obsłużyć błędów. Zawsze używaj Promise.all () w tym przypadku użycia. Async / await nie zastępuje obietnic. To po prostu ładny sposób, jak używać obietnic ... kod asynchroniczny jest napisany w stylu synchronizacji i możemy uniknąć wielu thenobietnic.

Niektórzy mówią, że używając Promise.all () nie możemy obsługiwać błędów zadań osobno, a jedynie błąd z pierwszej odrzuconej obietnicy (tak, niektóre przypadki użycia mogą wymagać oddzielnej obsługi np. Przy logowaniu). To nie jest problem - patrz nagłówek „Dodatek” poniżej.

Przykłady

Rozważ to zadanie asynchroniczne ...

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

Kiedy uruchamiasz zadania w pozytywnym scenariuszu, nie ma różnicy między Promise.all a Multiple await. Oba przykłady kończą się Task 1 succeed! Task 2 succeed!po 5 sekundach.

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

Kiedy pierwsze zadanie zajmuje 10 sekund w scenariuszu pozytywnym, a zadanie sekundowe zajmuje 5 sekund w scenariuszu negatywnym, występują różnice w wyświetlanych błędach.

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

Powinniśmy już zauważyć, że robimy coś złego, gdy używamy wielu czeków równolegle. Oczywiście, aby uniknąć błędów, powinniśmy sobie z tym poradzić! Spróbujmy...


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

Jak widać, aby pomyślnie obsłużyć błąd, musimy dodać tylko jeden catch do runfunkcji, a kod z logiką catch jest w wywołaniu zwrotnym ( styl async ). Nie potrzebujemy obsługi błędów wewnątrz runfunkcji, ponieważ funkcja asynchroniczna robi to automatycznie - obietnica odrzucenia taskfunkcji powoduje odrzucenie runfunkcji. Aby uniknąć wywołania zwrotnego, możemy użyć stylu synchronizacji (async / await + try / catch), try { await run(); } catch(err) { }ale w tym przykładzie nie jest to możliwe, ponieważ nie możemy użyć awaitw głównym wątku - można go używać tylko w funkcji async (jest to logiczne, ponieważ nikt nie chce blok główny wątek). Aby sprawdzić, czy obsługa działa w stylu synchronizacji , możemy wywołaćrunFunkcja innej funkcji asynchronicznej lub użytkowania Iife (bezpośrednio Wykonano wyrażenie funkcyjne) (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();.

To tylko jeden poprawny sposób uruchamiania dwóch lub więcej równoległych zadań asynchronicznych i obsługi błędów. Powinieneś unikać poniższych przykładów.


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

Możemy spróbować obsłużyć powyższy kod na kilka sposobów ...

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 

... nic nie zostało złapane, ponieważ obsługuje kod synchronizacji, ale runjest asynchroniczne

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? Najpierw widzimy, że błąd dla zadania 2 nie został obsłużony, a później został przechwycony. Wprowadzające w błąd i wciąż pełne błędów w konsoli. Nie do użytku w ten sposób.

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

... tak samo jak powyżej. Użytkownik @Qwerty w swojej usuniętej odpowiedzi zapytał o to dziwne zachowanie, które wydaje się być przechwytywane, ale są też nieobsłużone błędy. Łapiemy błąd, ponieważ run () jest odrzucany w linii ze słowem kluczowym await i może zostać przechwycony przy użyciu try / catch podczas wywoływania run (). Otrzymujemy również nieobsłużony błąd, ponieważ wywołujemy funkcję zadania asynchronicznego synchronicznie (bez słowa kluczowego await), a to zadanie jest uruchamiane poza funkcją run () i również kończy się niepowodzeniem na zewnątrz. Jest podobny, gdy nie jesteśmy w stanie obsłużyć błąd przez try / catch podczas wywoływania niektórych funkcji synchronizacji, która część uruchamia kod w 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)

... „tylko” dwa błędy (brakuje trzeciego), ale nic nie złapano.


Dodawanie (osobno obsługuj błędy zadań, a także błąd pierwszego niepowodzenia)

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!

... zwróć uwagę, że w tym przykładzie użyłem negatywnegoScenario = true dla obu zadań, aby lepiej zademonstrować, co się dzieje ( throw errsłuży do wywołania końcowego błędu)

mikep
źródło
14
ta odpowiedź jest lepsza niż zaakceptowana odpowiedź, ponieważ obecnie zaakceptowana odpowiedź pomija bardzo ważny temat obsługi błędów
chrishiestand
8

Ogólnie rzecz biorąc, Promise.all()równoległe uruchamianie żądań „asynchronicznych”. Używanie awaitmoże działać równolegle LUB blokować „synchronizację”.

Funkcje test1 i test2 poniżej pokazują, jak awaitmożna uruchomić async lub sync.

test3 pokazuje, Promise.all()że jest asynchroniczny.

jsfiddle z wynikami w czasie - otwórz konsolę przeglądarki, aby zobaczyć wyniki testów

Zachowanie synchronizacji . NIE działa równolegle, trwa ~ 1800 ms :

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

Zachowanie asynchroniczne . Działa równolegle, trwa ~ 600 ms :

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
}

Zachowanie asynchroniczne . Działa równolegle, trwa ~ 600 ms :

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

TLDR; Jeśli używasz Promise.all(), również "szybko zawiedzie" - przestanie działać w momencie pierwszej awarii którejkolwiek z dołączonych funkcji.

GavinBelson
źródło
1
Gdzie mogę uzyskać szczegółowe wyjaśnienie tego, co dzieje się pod maską we fragmentach 1 i 2? Jestem tak zaskoczony, że mają one inny sposób działania, ponieważ spodziewałem się, że zachowania będą takie same.
Gregordy
2
@Gregordy tak, to zaskakujące. Opublikowałem tę odpowiedź, aby oszczędzić nowym programistom asynchronizacji niektórych bólów głowy. Chodzi o to, kiedy JS ocenia oczekiwanie, dlatego liczy się sposób przypisywania zmiennych. W głębi asynchroniczny odczyt: blog.bitsrc.io/...
GavinBelson
7

Możesz sam sprawdzić.

Na tych skrzypcach przeprowadziłem test, aby zademonstrować blokującą naturę await, w przeciwieństwie do tego, Promise.allktóry rozpocznie wszystkie obietnice, a gdy jeden czeka, będzie kontynuowany z innymi.

zpr
źródło
6
Właściwie twoje skrzypce nie odpowiadają na jego pytanie. Istnieje różnica między wywołaniem, t1 = task1(); t2 = task2()a późniejszym użyciem awaitdla nich obu, result1 = await t1; result2 = await t2;jak w jego pytaniu, w przeciwieństwie do tego, co testujesz, czego używasz awaitw pierwotnym połączeniu result1 = await task1(); result2 = await task2();. Kod w jego pytaniu uruchamia wszystkie obietnice naraz. Różnica, jak pokazuje odpowiedź, polega na tym, że awarie będą zgłaszane szybciej po Promise.alldrodze.
BryanGrezeszak
Twoja odpowiedź jest poza tematem, na przykład skomentował @BryanGrezeszak. Powinieneś raczej go usunąć, aby uniknąć wprowadzania użytkowników w błąd.
mikep
0

W przypadku await Promise.all ([task1 (), task2 ()]); „zadanie1 ()” i „zadanie2 ()” będą działać równolegle i będą czekać, aż obie obietnice zostaną zakończone (rozwiązane lub odrzucone). Natomiast w przypadku

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

t2 będzie działać dopiero po zakończeniu wykonywania t1 (rozwiązaniu lub odrzuceniu). Zarówno t1, jak i t2 nie będą przebiegać równolegle.

Waleed Naveed
źródło