Qualche differenza tra await Promise.all () e multiple waitit?


Risposte:


210

Nota :

Questa risposta copre solo le differenze temporali tra awaitin serie e Promise.all. Assicurati di leggere la risposta completa di @ mikep che copre anche le differenze più importanti nella gestione degli errori .


Ai fini di questa risposta userò alcuni metodi di esempio:

  • res(ms) è una funzione che richiede un numero intero di millisecondi e restituisce una promessa che si risolve dopo molti millisecondi.
  • rej(ms) è una funzione che richiede un numero intero di millisecondi e restituisce una promessa che rifiuta dopo molti millisecondi.

La chiamata resavvia il timer. utilizzandoPromise.all di attendere una manciata di ritardi si risolverà dopo che tutti i ritardi saranno terminati, ma ricorda che vengono eseguiti contemporaneamente:

Esempio 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

Ciò significa che Promise.all si risolveranno con i dati delle promesse interne dopo 3 secondi.

Ma Promise.allha un comportamento "fail fast" :

Esempio n. 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

Se usi async-awaitinvece, dovrai aspettare che ogni promessa si risolva in sequenza, il che potrebbe non essere altrettanto efficiente:

Esempio n. 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


4
Quindi sostanzialmente la differenza è solo la funzione "fail fast" di Promise.all?
Matteo

4
@mclzc Nell'esempio n. 3 l'ulteriore esecuzione del codice viene interrotta fino alla risoluzione di delay1. È persino nel testo "Se usi invece async-waitit, dovrai aspettare che ogni promessa si risolva in sequenza"
haggis,

1
@Qback, c'è uno snippet di codice live che dimostra il comportamento. Considera di eseguirlo e rileggere il codice. Non sei la prima persona a fraintendere come si comporta la sequenza di promesse. L'errore che hai fatto nella tua demo è che non stai iniziando le tue promesse allo stesso tempo.
zzzzBov,

1
@zzzzBov Hai ragione. Lo stai iniziando nello stesso tempo. Mi dispiace, sono arrivato a questa domanda per un altro motivo e l'ho trascurato.
Qback

2
" potrebbe non essere così efficiente " - e, soprattutto, causare unhandledrejectionerrori. Non vorrai mai usare questo. Aggiungi questo alla tua risposta.
Bergi,

88

Prima differenza: fallisci velocemente

Sono d'accordo con la risposta di @ zzzzBov, ma il vantaggio di "Fail fast fast" di Promise.all non è solo la differenza. Alcuni utenti nei commenti chiedono perché utilizzare Promise.all quando è solo più veloce in uno scenario negativo (quando alcune attività falliscono). E chiedo perché no? Se ho due attività parallele asincrone indipendenti e la prima viene risolta in un tempo molto lungo ma la seconda viene rifiutata in brevissimo tempo perché lasciare all'utente attendere un messaggio di errore "molto lungo" anziché "molto breve"? Nelle applicazioni della vita reale dobbiamo considerare uno scenario negativo. Ma OK - in questa prima differenza puoi decidere quale alternativa utilizzare Promise.all rispetto a più aspetti.

Seconda differenza: gestione degli errori

Ma quando si considera la gestione degli errori DEVI usare Promise.all. Non è possibile gestire correttamente gli errori di attività parallele asincrone attivate con più attesa. In uno scenario negativo finirai sempre con UnhandledPromiseRejectionWarningePromiseRejectionHandledWarning sebbene tu usi try / catch ovunque. Ecco perché è stato progettato Promise.all. Certo qualcuno potrebbe dire che possiamo sopprimere gli errori usando process.on('unhandledRejection', err => {})e process.on('rejectionHandled', err => {})ma non è una buona pratica. Ho trovato molti esempi su Internet che non considerano affatto la gestione degli errori per due o più attività parallele asincrone indipendenti o la considerano, ma in modo errato: basta usare try / catch e sperare che rilevi errori. È quasi impossibile trovare buone pratiche. Ecco perché sto scrivendo questa risposta.

Sommario

Non utilizzare mai più wait per due o più attività parallele asincrone indipendenti perché non sarai in grado di gestire seriamente gli errori. Utilizzare sempre Promise.all () per questo caso d'uso. Async / await non sostituisce Promises. È semplicemente un modo carino per usare le promesse ... il codice asincrono è scritto in stile sync e possiamo evitarne il multiplothen nelle promesse.

Alcune persone affermano che utilizzando Promise.all () non possiamo gestire gli errori delle attività separatamente ma solo l'errore della prima promessa respinta (sì, alcuni casi d'uso potrebbero richiedere una gestione separata, ad esempio per la registrazione). Non è un problema - vedi sotto "Aggiunta" sotto.

Esempi

Considera questa attività asincrona ...

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

Quando si eseguono attività in uno scenario positivo, non vi è alcuna differenza tra Promise.all e più wait. Entrambi gli esempi terminano Task 1 succeed! Task 2 succeed!dopo 5 secondi.

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

Quando la prima attività richiede 10 secondi in uno scenario positivo e l'attività secondi richiede 5 secondi in uno scenario negativo, ci sono differenze negli errori emessi.

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

Dovremmo già notare qui che stiamo facendo qualcosa di sbagliato quando si usano più aspetti in parallelo. Naturalmente per evitare errori dovremmo gestirlo! Proviamo...


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

Come si può vedere per gestire correttamente l'errore, è necessario aggiungere solo un catch alla runfunzione e il codice con la logica catch è in callback ( stile asincrono ). Non abbiamo bisogno di gestire gli errori all'interno della runfunzione perché la funzione asincrona lo fa automaticamente - promettere il rifiuto della taskfunzione provoca il rifiuto della runfunzione. Per evitare il callback possiamo usare lo stile di sincronizzazione (async / await + try / catch) try { await run(); } catch(err) { }ma in questo esempio non è possibile perché non possiamo usare awaitnel thread principale - può essere usato solo nella funzione asincrona (è logico perché nessuno vuole bloccare il filo principale). Per verificare se la gestione funziona nello stile di sincronizzazione, possiamo chiamarerunFunzione da un'altra funzione asincrona o uso IIFE (Subito Richiamato espressione di funzione): (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();.

Questo è solo un modo corretto per eseguire due o più attività parallele asincrone e gestire gli errori. Dovresti evitare esempi di seguito.


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

Possiamo provare a gestire il codice sopra diversi modi ...

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 

... nulla è stato rilevato perché gestisce il codice di sincronizzazione ma runè asincrono

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? Innanzitutto vediamo che l'errore per l'attività 2 non è stato gestito e successivamente è stato rilevato. Ingannevole e ancora pieno di errori nella console. Inutilizzabile in questo modo.

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

... come sopra. L'utente @Qwerty nella sua risposta eliminata ha chiesto di questo strano comportamento che sembra essere stato colto ma ci sono anche errori non gestiti. Si rileva un errore perché run () viene rifiutato in linea con la parola chiave wait e può essere rilevato utilizzando try / catch quando si chiama run (). Riceviamo anche errori non gestiti perché stiamo chiamando la funzione di attività asincrona in modo sincrono (senza la parola chiave wait) e questa attività viene eseguita al di fuori della funzione run () e fallisce anche all'esterno. E 'simile quando non siamo in grado di gestire errore try / catch quando si chiama una funzione di sincronizzazione quale parte di piste di codice 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)

... "solo" due errori (manca il terzo) ma non è stato rilevato nulla.


Aggiunta (gestire gli errori dell'attività separatamente e anche errore first-fail)

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!

... nota che in questo esempio ho usato negativeScenario = true per entrambe le attività per una migliore dimostrazione di ciò che accade ( throw errviene utilizzato per generare l'errore finale)


14
questa risposta è migliore della risposta accettata perché la risposta attualmente accettata manca l'argomento molto importante della gestione degli errori
chrishiestand

8

Generalmente, utilizzando le Promise.all()richieste di esecuzione "asincrono" in parallelo. L'uso awaitpuò essere eseguito in parallelo O essere il blocco "sync".

test1 e test2 funzioni sottostanti mostrano come awaitpuò funzionare asincrona o la sincronizzazione.

test3 mostra Promise.all()che è asincrono.

jsfiddle con risultati a tempo - apri la console del browser per vedere i risultati dei test

Sincronizza comportamento. NON funziona in parallelo, richiede ~ 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
};

Comportamento asincrono . Funziona in parallelo, richiede ~ 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
}

Comportamento asincrono . Funziona in parallelo, richiede ~ 600ms :

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

TLDR; Se lo stai usando Promise.all()anche "fast-fail" - interromperà l'esecuzione al momento del primo errore di una qualsiasi delle funzioni incluse.


1
Dove posso ottenere una spiegazione dettagliata di ciò che accade sotto il cofano negli snippet 1 e 2? Sono così sorpreso che questi abbiano un modo diverso di correre poiché mi aspettavo che i comportamenti fossero gli stessi.
Gregordy,

2
@Gregordy sì, è sorprendente. Ho pubblicato questa risposta per salvare i programmatori nuovi che asincrono alcuni mal di testa. È tutto su quando JS valuta l'attesa, ecco perché il modo in cui assegni le variabili è importante. Approfondimento Async reading: blog.bitsrc.io/…
GavinBelson

7

Puoi controllare tu stesso.

In questo violino , ho eseguito un test per dimostrare la natura bloccante di await, al contrario del Promise.allquale inizieranno tutte le promesse e mentre uno sta aspettando andrà avanti con gli altri.


6
In realtà, il tuo violino non risponde alla sua domanda. C'è una differenza tra chiamare t1 = task1(); t2 = task2()e poi usare in awaitseguito per entrambi result1 = await t1; result2 = await t2;come nella sua domanda, al contrario di ciò che stai testando che sta usando awaitnella chiamata originale come result1 = await task1(); result2 = await task2();. Il codice nella sua domanda inizia tutte le promesse in una volta. La differenza, come mostra la risposta, è che i guasti verranno segnalati più rapidamente Promise.all.
BryanGrezeszak,

La tua risposta è fuori tema come @BryanGrezeszak ha commentato. Dovresti piuttosto eliminarlo per evitare utenti fuorvianti.
mikep,

0

In caso di wait Promise.all ([task1 (), task2 ()]); "task1 ()" e "task2 ()" verranno eseguiti in parallelo e aspetteranno fino al completamento di entrambe le promesse (risolte o rifiutate). Considerando che in caso di

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

t2 verrà eseguito solo dopo che t1 ha terminato l'esecuzione (è stato risolto o rifiutato). Sia t1 che t2 non funzioneranno in parallelo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.