Chiamare le funzioni asincrone / wait in parallelo


434

Per quanto ho capito, in ES7 / ES2016 mettere multipli awaitnel codice funzionerà in modo simile al concatenarsi .then()con le promesse, nel senso che eseguiranno una dopo l'altra piuttosto che in parallelo. Quindi, ad esempio, abbiamo questo codice:

await someCall();
await anotherCall();

Capisco correttamente che anotherCall()verrà chiamato solo quando someCall()sarà completato? Qual è il modo più elegante di chiamarli in parallelo?

Voglio usarlo in Node, quindi forse c'è una soluzione con la libreria asincrona?

EDIT: Non sono soddisfatto della soluzione fornita in questa domanda: rallentamento dovuto all'attesa non parallela delle promesse nei generatori asincroni , perché utilizza generatori e sto chiedendo un caso d'uso più generale.


1
@adeneo Questo non è corretto, Javascript non funziona mai in parallelo all'interno del proprio contesto.
Blindman67,

5
@ Blindman67 - almeno il modo in cui significa OP, in cui due operazioni asincrone sono in esecuzione simultaneamente, ma non in questo caso, ciò che intendevo scrivere era che funzionano in seriale , il primo awaitaspetterebbe il completamento della prima funzione interamente prima di eseguire il secondo.
adeneo,

3
@ Blindman67 - è a thread singolo, ma quella limitazione non si applica ai metodi asincroni, possono essere eseguiti simultaneamente e restituire la risposta quando hanno finito, cioè cosa significa OP in "parallelo".
adeneo,

7
@ Blindman67 - Penso che sia abbastanza chiaro ciò che l'OP sta chiedendo, l'uso del modello asincrono / wait farà funzionare le funzioni in serie, anche se sono asincrone, quindi il primo finirà completamente prima che il secondo venga chiamato ecc. L'OP è chiedendo come chiamare entrambe le funzioni in parallelo, e poiché sono chiaramente asincroni, l'obiettivo è eseguirle simultaneamente, cioè in parallelo, ad esempio facendo contemporaneamente due richieste ajax, il che non è affatto un problema in JavaScript, come la maggior parte dei metodi asincroni , come hai notato, esegue il codice nativo e utilizza più thread.
adeneo,

3
@Bergi questo non è un duplicato della domanda collegata - si tratta in particolare della sintassi asincrona / waitit e dei messaggi nativi Promise. La domanda collegata riguarda la libreria bluebird con generatori e rendimento. Concettualmente simile forse, ma non in fase di attuazione.
IEST

Risposte:


703

Puoi aspettare su Promise.all():

await Promise.all([someCall(), anotherCall()]);

Per memorizzare i risultati:

let [someResult, anotherResult] = await Promise.all([someCall(), anotherCall()]);

Nota che Promise.allfallisce velocemente, il che significa che non appena una delle promesse fornite ad essa viene respinta, l'intera cosa rifiuta.

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'

Se, invece, vuoi aspettare che tutte le promesse siano rispettate o respinte, allora puoi usare Promise.allSettled. Si noti che Internet Explorer non supporta nativamente questo metodo.

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" }]


79
Pulito ma sii consapevole del comportamento rapido di Promise.all. Se una delle funzioni genera un errore, Promise.all rifiuterà
NoNameProvided

11
Puoi gestire correttamente i risultati parziali con asincrono / attendi, vedi stackoverflow.com/a/42158854/2019689
NoName Fornito

131
Suggerimento pro: usa l'array destructuring per inizializzare un numero arbitrario di risultati da Promise.all (), come:[result1, result2] = Promise.all([async1(), async2()]);
jonny

10
@jonny Questo argomento è soggetto a fail-fast? Inoltre, è ancora necessario = await Promise.all?
theUtherSide

5
@theUtherSide Hai perfettamente ragione - ho trascurato di includere l'attesa.
Jonny,

114

TL; DR

Utilizzare Promise.allper le chiamate di funzione parallele, la risposta si comporta in modo non corretto quando si verifica l'errore.


Innanzitutto, esegui tutte le chiamate asincrone contemporaneamente e ottieni tutti gli Promiseoggetti. In secondo luogo, utilizzare awaitsugli Promiseoggetti. In questo modo, mentre aspetti che il primo si Promiserisolva, le altre chiamate asincrone procedono ancora. Complessivamente, attenderai fino a quando la chiamata asincrona più lenta. Per esempio:

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

Esempio di JSbin: http://jsbin.com/xerifanima/edit?js,console

Avvertenza: non importa se le awaitchiamate si trovano sulla stessa linea o su linee diverse, a condizione che la prima awaitchiamata avvenga dopo tutte le chiamate asincrone. Vedi il commento di JohnnyHK.


Aggiornamento: questa risposta ha una diversa tempistica nella gestione degli errori in base alla risposta di @ bergi , NON genera l'errore man mano che si verifica l'errore ma dopo che tutte le promesse sono state eseguite. Confronto il risultato con il suggerimento di @ jonny:, [result1, result2] = Promise.all([async1(), async2()])controlla il seguente frammento di codice

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
Questa mi sembra un'opzione molto più bella di Promise.all - e con compiti distruttivi puoi anche fare [someResult, anotherResult] = [await someResult, await anotherResult]se cambi consta let.
jawj,

28
Ma questo esegue ancora le awaitdichiarazioni in serie, giusto? Cioè, l'esecuzione si interrompe fino a quando il primo non si awaitrisolve, quindi passa al secondo. Promise.allesegue in parallelo.
Andru,

8
Grazie @Haven. Questa dovrebbe essere la risposta accettata.
Stefan D,

87
Questa risposta è fuorviante in quanto il fatto che entrambe le attese siano fatte sulla stessa linea è irrilevante. Ciò che conta è che le due chiamate asincrone vengano effettuate prima che l'una o l'altra sia attesa.
JohnnyHK,

15
@Haven questa soluzione non è la stessa di Promise.all. Se ogni richiesta è una chiamata di rete, await someResultdovrà essere risolta prima await anotherResultancora che venga avviata. Al contrario, nelle Promise.alldue awaitchiamate può essere avviato prima che uno dei due sia risolto.
Ben Winding,

89

Aggiornare:

La risposta originale rende difficile (e in alcuni casi impossibile) gestire correttamente i rifiuti promettenti. La soluzione corretta è usare Promise.all:

const [someResult, anotherResult] = await Promise.all([someCall(), anotherCall()]);

Risposta originale:

Assicurati di chiamare entrambe le funzioni prima di attendere una delle due:

// Call both functions
const somePromise = someCall();
const anotherPromise = anotherCall();

// Await both promises    
const someResult = await somePromise;
const anotherResult = await anotherPromise;

1
@JeffFischer Ho aggiunto commenti che, si spera, renderanno più chiaro.
Jonathan Potter,

9
Sento che questa è sicuramente la risposta più pura
Gershom,

1
Questa risposta è molto più chiara di quella di Haven. È chiaro che le chiamate di funzione restituiranno gli oggetti promessa e awaitli risolveranno in valori effettivi.
user1032613

3
Questo sembra funzionare a colpo d'occhio, ma ha problemi orribili con rifiuti non gestiti . Non usare questo!
Bergi,

1
@Bergi Hai ragione, grazie per averlo sottolineato! Ho aggiornato la risposta con una soluzione migliore.
Jonathan Potter,

24

Esiste un altro modo senza Promise.all () per farlo in parallelo:

Innanzitutto, abbiamo 2 funzioni per stampare i numeri:

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

Questo è sequenziale:

async function oneByOne() {
   const number1 = await printNumber1();
   const number2 = await printNumber2();
} 
//Output: Number1 is done, Number2 is done

Questo è parallelo:

async function inParallel() {
   const promise1 = printNumber1();
   const promise2 = printNumber2();
   const number1 = await promise1;
   const number2 = await promise2;
}
//Output: Number2 is done, Number1 is done

10

Questo può essere realizzato con Promise.allSettled () , che è simile Promise.all()ma senza il comportamento fail-fast.

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!"}

Nota : questa è una funzionalità all'avanguardia con supporto del browser limitato, quindi consiglio vivamente di includere un polyfill per questa funzione.



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

Sebbene l'impostazione di p1, p2 e p3 non li esegua rigorosamente in parallelo, non trattengono alcuna esecuzione e è possibile intercettare errori contestuali con una cattura.


2
Benvenuto in Stack Overflow. Mentre il tuo codice può fornire la risposta alla domanda, ti preghiamo di aggiungere un contesto attorno ad esso in modo che altri abbiano qualche idea di cosa fa e perché è lì.
Theo,

1

Nel mio caso, ho diverse attività che voglio eseguire in parallelo, ma ho bisogno di fare qualcosa di diverso con il risultato di tali attività.

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

E l'output:

Starting task: moose 1000
Starting task: taco 500
Starting task: burp 5000
taco
moose
burp
done

fantastico per la creazione dinamica (matrice di risorse)
Michal Miky Jankovský

1

await Promise.all ([someCall (), anotherCall ()]); come già accennato fungerà da recinto di thread (molto comune in codice parallelo come CUDA), quindi consentirà a tutte le promesse in esso eseguite di bloccarsi a vicenda, ma impedirà l'esecuzione fino a quando TUTTI non saranno risolti.

un altro approccio che vale la pena condividere è l'asincronizzazione Node.js che ti consentirà anche di controllare facilmente la quantità di concorrenza che è di solito desiderabile se l'attività è direttamente collegata all'uso di risorse limitate come chiamata API, operazioni I / O, eccetera.

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

Crediti all'autore dell'articolo Medium ( leggi di più )


-5

Voto per:

await Promise.all([someCall(), anotherCall()]);

Fare attenzione al momento in cui si chiamano le funzioni, potrebbe causare risultati imprevisti:

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

Ma il seguito innesca sempre la richiesta di creare un nuovo utente

// 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]
}

Dal momento che si dichiara la funzione al di fuori / prima del test delle condizioni e li ha chiamati. Prova a avvolgerli in elseblocchi.
Haven,

@Haven: intendo quando separi i momenti che chiami funzioni vs wait possono portare a risultati inaspettati, ad esempio: richieste HTTP asincrone.
Hoang Le Anh Tu

-6

Creo una funzione di aiuto waitAll, può essere può renderlo più dolce. Funziona solo con nodejs per ora, non con Chrome per browser.

    //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
No, la parallelizzazione non sta accadendo affatto qui. Il forciclo attende sequenzialmente ogni promessa e aggiunge il risultato alla matrice.
Szczepan Hołyszewski,

Capisco che questo non sembra funzionare per le persone. Quindi ho provato su node.js e browser. Il test viene passato in node.js (v10, v11), firefox, non funziona nel browser Chrome. Il caso di prova è in gist.github.com/fredyang/ea736a7b8293edf7a1a25c39c7d2fbbf
Fred Yang,

2
Mi rifiuto di crederci. Non c'è nulla nello standard che dice che diverse iterazioni di un ciclo for possano essere parallelizzate automagicamente; non è così che funziona javascript. Il modo in cui il codice del ciclo è scritto, significa questo: "aspetta un elemento (l'atteso expr), POI spingi il risultato in temp, POI prendi l'elemento successivo (iterazione successiva del ciclo for). L '" attesa "per ogni elemento è completamente limitato a una singola iterazione del loop. Se i test mostrano che esiste una parallelizzazione, deve essere perché il transpiler sta facendo qualcosa di non standard o è completamente difettoso
Szczepan Hołyszewski

@ SzczepanHołyszewski La tua sicurezza di non credere senza eseguire il caso di prova mi ispira a fare qualche rinominazione di refettorio e altri commenti. Tutti i codici sono semplici ES6, non è richiesta la traspilazione.
Fred Yang,

Non sono sicuro del motivo per cui questo è stato sottoposto a downgrade così pesantemente. È essenzialmente la stessa risposta che @ user2883596 ha dato.
Jonathan Sudiaman,
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.