Rufen Sie parallel asynchrone / warten-Funktionen auf

429

Soweit ich weiß, funktioniert das Einfügen mehrerer awaitCodes in ES7 / ES2016 ähnlich wie das Verketten .then()mit Versprechungen, was bedeutet, dass sie nacheinander und nicht parallel ausgeführt werden. So haben wir zum Beispiel diesen Code:

await someCall();
await anotherCall();

Verstehe ich es richtig, dass anotherCall()erst aufgerufen wird, wenn someCall()es abgeschlossen ist? Was ist die eleganteste Art, sie parallel zu nennen?

Ich möchte es in Node verwenden, also gibt es vielleicht eine Lösung mit asynchroner Bibliothek?

BEARBEITEN: Ich bin mit der in dieser Frage bereitgestellten Lösung nicht zufrieden: Verlangsamung aufgrund des nicht parallelen Wartens auf Versprechen in asynchronen Generatoren , da Generatoren verwendet werden und ich nach einem allgemeineren Anwendungsfall frage.

Victor Marchuk
quelle
1
@adeneo Das ist falsch, Javascript läuft nie parallel in seinem eigenen Kontext.
Blindman67
5
@ Blindman67 - es funktioniert zumindest so, wie das OP bedeutet, dass zwei asynchrone Operationen gleichzeitig ausgeführt werden, aber in diesem Fall wollte ich nicht schreiben, dass sie seriell ausgeführt werden. Die erste awaitwürde warten, bis die erste Funktion abgeschlossen ist vollständig vor der Ausführung der zweiten.
Adeneo
3
@ Blindman67 - es ist Single-Threaded, aber diese Einschränkung gilt nicht für asynchrone Methoden. Sie können gleichzeitig ausgeführt werden und die Antwort zurückgeben, wenn sie fertig sind, dh was das OP mit "Parallell" bedeutet.
Adeneo
7
@ Blindman67 - Ich denke, es ist ziemlich klar, was das OP verlangt. Wenn Sie das Async / Wait-Muster verwenden, werden die Funktionen seriell ausgeführt, auch wenn sie asynchron sind, sodass die erste vollständig beendet wird, bevor die zweite aufgerufen wird usw. Das OP ist Bei der Frage, wie beide Funktionen in Parallell aufgerufen werden sollen, und da sie eindeutig asynchron sind, besteht das Ziel darin, sie gleichzeitig auszuführen, dh in Parallell, beispielsweise zwei Ajax-Anforderungen gleichzeitig auszuführen, was in Javascript wie bei den meisten asynchronen Methoden überhaupt kein Problem darstellt Wie bereits erwähnt, wird nativer Code ausgeführt und es werden mehr Threads verwendet.
Adeneo
3
@Bergi Dies ist kein Duplikat der verknüpften Frage - hier geht es speziell um asynchrone / wartende Syntax und native Promises. Die verknüpfte Frage betrifft die Bluebird-Bibliothek mit Generatoren und Ertrag. Konzeptionell vielleicht ähnlich, aber nicht in der Umsetzung.
Iest

Antworten:

694

Sie können warten auf Promise.all():

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

So speichern Sie die Ergebnisse:

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

Beachten Sie, dass Promise.alldies schnell fehlschlägt. Dies bedeutet, dass das gesamte Objekt abgelehnt wird, sobald eines der ihm gegebenen Versprechen abgelehnt wird.

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'

Wenn Sie stattdessen darauf warten möchten, dass alle Versprechen erfüllt oder abgelehnt werden, können Sie sie verwenden Promise.allSettled. Beachten Sie, dass Internet Explorer diese Methode nicht nativ unterstützt.

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

madox2
quelle
78
Sauber, aber beachten Sie das Fast-Fail-Verhalten von Promise.all. Wenn eine der Funktionen einen Fehler auslöst, wird Promise.all ablehnen
NoNameProvided
11
Sie können Teilergebnisse mit async / await gut verarbeiten, siehe stackoverflow.com/a/42158854/2019689
NoNameProvided
131
Pro-Tipp: Verwenden Sie die Array-Destrukturierung, um eine beliebige Anzahl von Ergebnissen aus Promise.all () zu initialisieren, z. B.:[result1, result2] = Promise.all([async1(), async2()]);
10.
10
@jonny Ist dieses Thema ausfallsicher? Muss man das auch noch = await Promise.all?
theUtherSide
5
@theUtherSide Du hast absolut Recht - ich habe es versäumt, das Warten einzuschließen.
Jonny
114

TL; DR

Verwenden Sie Promise.allfür die parallelen Funktionsaufrufe das Antwortverhalten nicht korrekt, wenn der Fehler auftritt.


Führen Sie zunächst alle asynchronen Aufrufe gleichzeitig aus und erhalten Sie alle PromiseObjekte. Zweitens awaitfür die PromiseObjekte verwenden. Auf diese Weise werden Promisedie anderen asynchronen Aufrufe fortgesetzt , während Sie darauf warten, dass der erste die anderen asynchronen Aufrufe auflöst. Insgesamt warten Sie nur so lange wie der langsamste asynchrone Anruf. Zum Beispiel:

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

JSbin-Beispiel: http://jsbin.com/xerifanima/edit?js,console

Vorsichtsmaßnahme: Es spielt keine Rolle, ob sich die awaitAnrufe auf derselben Leitung oder auf verschiedenen Leitungen befinden, solange der erste awaitAnruf nach allen asynchronen Anrufen erfolgt. Siehe JohnnyHKs Kommentar.


Update: Diese Antwort hat ein anderes Timing bei der Fehlerbehandlung als die Antwort von @ bergi . Sie wirft den Fehler NICHT aus, wenn der Fehler auftritt, sondern nachdem alle Versprechen ausgeführt wurden. Ich vergleiche das Ergebnis mit @ jonnys Tipp: [result1, result2] = Promise.all([async1(), async2()])Überprüfen Sie das folgende Code-Snippet

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

Oase
quelle
11
Das sieht aus wie eine viel schönere Option für mich , als Promise.all - und mit Zuordnung Destrukturierung Sie selbst tun können , [someResult, anotherResult] = [await someResult, await anotherResult]wenn Sie ändern constzu let.
jawj
28
Aber das führt die awaitAussagen immer noch seriell aus, oder? Das heißt, die Ausführung wird angehalten, bis die erste aufgelöst ist await, und geht dann zur zweiten über. Promise.allwird parallel ausgeführt.
Andru
8
Danke @Haven. Dies sollte die akzeptierte Antwort sein.
Stefan D
87
Diese Antwort ist irreführend, da die Tatsache, dass beide Wartezeiten in derselben Zeile erfolgen, irrelevant ist. Was zählt, ist, dass die beiden asynchronen Anrufe getätigt werden, bevor einer von beiden erwartet wird.
JohnnyHK
15
@Haven diese Lösung ist nicht die gleiche wie Promise.all. Wenn es sich bei jeder Anforderung um einen Netzwerkanruf handelt, await someResultmuss dieser gelöst werden, bevor er await anotherResultüberhaupt gestartet wird. Umgekehrt können in Promise.allden beiden awaitAufrufen gestartet werden, bevor einer der beiden aufgelöst wird.
Ben Winding
89

Aktualisieren:

Die ursprüngliche Antwort macht es schwierig (und in einigen Fällen unmöglich), Versprechungen abzulehnen. Die richtige Lösung ist zu verwenden Promise.all:

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

Ursprüngliche Antwort:

Stellen Sie einfach sicher, dass Sie beide Funktionen aufrufen, bevor Sie auf eine der beiden warten:

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

// Await both promises    
const someResult = await somePromise;
const anotherResult = await anotherPromise;
Jonathan Potter
quelle
1
@ JeffFischer Ich habe Kommentare hinzugefügt, die es hoffentlich klarer machen.
Jonathan Potter
9
Ich denke, dies ist sicherlich die reinste Antwort
Gershom
1
Diese Antwort ist viel klarer als die von Haven. Es ist klar, dass die Funktionsaufrufe Versprechenobjekte zurückgeben und awaitsie dann in tatsächliche Werte auflösen.
user1032613
3
Dies scheint auf einen flüchtigen Blick zu funktionieren, hat aber schreckliche Probleme mit unbehandelten Ablehnungen . Verwenden Sie dies nicht!
Bergi
1
@Bergi Du hast recht, danke, dass du darauf hingewiesen hast! Ich habe die Antwort mit einer besseren Lösung aktualisiert.
Jonathan Potter
24

Es gibt einen anderen Weg ohne Promise.all (), dies parallel zu tun:

Erstens haben wir zwei Funktionen zum Drucken von Zahlen:

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

Dies ist sequentiell:

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

Das ist parallel:

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

Dies kann mit Promise.allSettled () erreicht werden , das Promise.all()dem ausfallsicheren Verhalten ähnelt, jedoch ohne dieses ausfällt.

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

Hinweis : Dies ist eine hochmoderne Funktion mit eingeschränkter Browserunterstützung. Ich empfehle daher dringend , für diese Funktion eine Polyfüllung hinzuzufügen.

Jonathan Sudiaman
quelle
7

Ich habe eine Übersicht erstellt , in der verschiedene Methoden zur Lösung von Versprechungen mit Ergebnissen getestet wurden. Es kann hilfreich sein, die Optionen anzuzeigen, die funktionieren.

SkarXa
quelle
Die Tests 4 und 6 im Kern lieferten die erwarteten Ergebnisse. Siehe stackoverflow.com/a/42158854/5683904 von NoNameProvided, der den Unterschied zwischen den Optionen erklärt.
Akraines
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
    */

Während das Setzen von p1, p2 und p3 sie nicht streng parallel ausführt, halten sie keine Ausführung auf und Sie können Kontextfehler mit einem Catch abfangen.

Thrunobulax
quelle
2
Willkommen bei Stack Overflow. Während Ihr Code möglicherweise die Antwort auf die Frage liefert, fügen Sie bitte einen Kontext hinzu, damit andere eine Vorstellung davon haben, was er tut und warum er dort ist.
Theo
1

warte auf Promise.all ([someCall (), anotherCall ()]); Wie bereits erwähnt, fungiert es als Thread-Zaun (im parallelen Code als CUDA sehr häufig), sodass alle darin enthaltenen Versprechen ausgeführt werden können, ohne sich gegenseitig zu blockieren. Es wird jedoch verhindert, dass die Ausführung fortgesetzt wird, bis ALLE gelöst sind.

Ein weiterer Ansatz, der es wert ist, geteilt zu werden, ist die asynchrone Node.js-Methode, mit der Sie auch die Menge an Parallelität steuern können, die normalerweise wünschenswert ist, wenn die Aufgabe direkt mit der Verwendung begrenzter Ressourcen als API-Aufruf, E / A-Operationen, verknüpft ist. usw.

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

Credits für den Autor des Medium-Artikels ( lesen Sie mehr )

Thiago Conrado
quelle
0

In meinem Fall habe ich mehrere Aufgaben, die ich parallel ausführen möchte, aber ich muss mit dem Ergebnis dieser Aufgaben etwas anderes tun.

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

Und die Ausgabe:

Starting task: moose 1000
Starting task: taco 500
Starting task: burp 5000
taco
moose
burp
done
Alex Dresko
quelle
-5

Ich stimme für:

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

Beachten Sie den Moment, in dem Sie Funktionen aufrufen. Dies kann zu unerwarteten Ergebnissen führen:

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

Das Folgen löst jedoch immer die Anforderung aus, einen neuen Benutzer zu erstellen

// 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]
}
Hoang Le Anh Tu
quelle
Da deklarieren Sie die Funktion außerhalb / vor dem Bedingungstest und rufen sie auf. Versuchen Sie, sie in elseBlock zu wickeln .
Haven
@Haven: Ich meine , wenn Sie die Momente , trennen Sie rufen Funktionen vs await zu unerwarteten Ergebnissen führen können, zum Beispiel: Asynchron - HTTP - Anfragen.
Hoang Le Anh Tu
-6

Ich erstelle eine Hilfsfunktion waitAll, vielleicht kann sie süßer werden. Es funktioniert momentan nur in NodeJS , nicht in Browser Chrome.

    //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());
Fred Yang
quelle
3
Nein, Parallelisierung findet hier überhaupt nicht statt. Die forSchleife wartet nacheinander auf jedes Versprechen und fügt das Ergebnis dem Array hinzu.
Szczepan Hołyszewski
Ich verstehe, dass dies für Menschen nicht funktioniert. Also habe ich in node.js und im Browser getestet. Der Test wird in node.js (v10, v11), Firefox, bestanden, es funktioniert nicht in Browser Chrome. Der Testfall ist in gist.github.com/fredyang/ea736a7b8293edf7a1a25c39c7d2fbbf
Fred Yang
2
Ich weigere mich, das zu glauben. Der Standard enthält nichts, was besagt, dass verschiedene Iterationen einer for-Schleife automatisch parallelisiert werden können. So funktioniert Javascript nicht. Die Art und Weise, wie der Schleifencode geschrieben wird, bedeutet Folgendes: "Warten Sie auf ein Element (das Warten auf Ausdruck), DANN drücken Sie das Ergebnis auf Temp, DANN nehmen Sie das nächste Element (nächste Iteration der for-Schleife). Das" Warten "für jedes Element ist vollständig beschränkt auf eine einzelne Iteration der Schleife. Wenn Tests zeigen, dass es eine Parallelisierung gibt, muss dies daran liegen, dass der Transpiler etwas tut, das nicht dem
Standard entspricht
@ SzczepanHołyszewski Ihr Vertrauen, ohne den Testfall auszuführen, inspiriert mich dazu, einige Umbenennungen vorzunehmen und zusätzliche Kommentare abzugeben. Alle Codes sind alte ES6, kein Transpiling ist erforderlich.
Fred Yang
Ich bin mir nicht sicher, warum dies so stark abgelehnt wird. Es ist im Wesentlichen die gleiche Antwort, die @ user2883596 gegeben hat.
Jonathan Sudiaman