Abschnitt 6 · Lektion 4
Parallel oder nacheinander
Du weißt jetzt, dass Warten nichts kostet. Der häufigste Fehler in echtem Node-Code ist trotzdem genau der, bei dem gewartet wird, obwohl niemand warten müsste. Und er sieht harmlos aus.
Die Schleife, die keiner für langsam hält
const holen = (nummer) =>
new Promise((fertig) => setTimeout(() => fertig(`Datei ${nummer}`), 100));
const start = performance.now();
const ergebnisse = [];
for (const nummer of [1, 2, 3, 4, 5]) {
ergebnisse.push(await holen(nummer));
}
console.log(`${ergebnisse.length} geholt`);
console.log(`Gedauert: etwa ${Math.round((performance.now() - start) / 100) * 100} ms`); Fünf Vorgänge zu je hundert Millisekunden, macht fünfhundert. Das steht so da und ist sofort
einleuchtend, sobald man es einmal gesehen hat. Beim Schreiben sieht man es nicht, weil await in
einer Schleife völlig normal aussieht.
Bei fünf Dateien fällt das niemandem auf. Bei zwanzig sind es zwei Sekunden statt hundert Millisekunden, und dann wundert man sich, warum die Seite so lahm ist.
Erst alle starten, dann einmal warten
const holen = (nummer) =>
new Promise((fertig) => setTimeout(() => fertig(`Datei ${nummer}`), 100));
const start = performance.now();
const ergebnisse = await Promise.all([1, 2, 3, 4, 5].map(holen));
console.log(`${ergebnisse.length} geholt`);
console.log(`Gedauert: etwa ${Math.round((performance.now() - start) / 100) * 100} ms`); Der Unterschied steckt in einer Kleinigkeit: [1, 2, 3, 4, 5].map(holen) ruft holen fünfmal auf,
ohne dazwischen zu warten. Damit laufen fünf Vorgänge, und Promise.all wartet einmal auf alle
zusammen.
Das Ergebnis ist ein Array in derselben Reihenfolge wie die Eingabe. Wer zuerst fertig ist, spielt keine Rolle, du bekommst die Ergebnisse sortiert zurück, wie du sie hineingegeben hast.
Der Haken an Promise.all
Promise.all ist ein Alles-oder-nichts-Versprechen.
const holen = (nummer) =>
nummer === 3
? Promise.reject(new Error("Datei 3 fehlt"))
: Promise.resolve(`Datei ${nummer}`);
try {
const ergebnisse = await Promise.all([1, 2, 3, 4, 5].map(holen));
console.log(`${ergebnisse.length} geholt`);
} catch (fehler) {
console.log(`Abgebrochen: ${fehler.message}`);
console.log("Von den vier heilen Dateien ist nichts uebrig.");
} Sobald ein einziges Promise scheitert, wirft Promise.all sofort. Die vier anderen sind gelaufen
oder laufen noch, aber du kommst an ihre Ergebnisse nicht mehr heran. Das Promise, das dir sie
gegeben hätte, ist bereits im Fehlerzustand.
Wenn dein Programm ohne alle Teile ohnehin nicht weiterkann, ist das genau richtig. Wenn nicht, ist es ein Datenverlust auf Ansage.
allSettled, wenn jedes Ergebnis für sich zählt
const holen = (nummer) =>
nummer === 3
? Promise.reject(new Error("Datei 3 fehlt"))
: Promise.resolve(`Datei ${nummer}`);
const ergebnisse = await Promise.allSettled([1, 2, 3, 4, 5].map(holen));
for (const eintrag of ergebnisse) {
console.log(
eintrag.status === "fulfilled"
? `ok: ${eintrag.value}`
: `fehlt: ${eintrag.reason.message}`,
);
} Promise.allSettled wartet ab, bis alle fertig sind, und wirft nie. Du bekommst für jedes ein
Objekt: status ist fulfilled oder rejected, und dazu gibt es entweder ein value oder ein
reason.
Der Preis ist, dass du die Ergebnisse selbst auseinandersortieren musst. Dafür verlierst du nichts.
Die beiden Geschwister dazu, je in einem Satz: Promise.race gibt dir das erste Ergebnis, egal ob
Erfolg oder Fehler, und ist der übliche Weg für eine Zeitgrenze. Promise.any gibt dir das erste
erfolgreiche und scheitert nur, wenn alle scheitern.
Wann nacheinander richtig ist
Gleichzeitig ist nicht immer besser. Nacheinander ist richtig, wenn der zweite Vorgang das Ergebnis des ersten braucht. Erst anmelden, dann mit der Kennung weiterarbeiten, da hilft keine Gleichzeitigkeit.
Und es ist richtig, wenn die Gegenstelle nicht mehr verträgt. Eine fremde Schnittstelle mit einem Limit von zehn Anfragen pro Sekunde freut sich nicht über zweihundert auf einmal, und eine Datenbank mit zwanzig Verbindungen im Pool auch nicht.
Damit zur Zahl, die man im Kopf haben sollte: „alle gleichzeitig” heißt bei fünftausend Dateien
wirklich fünftausend gleichzeitig. Promise.all fragt nicht nach, ob das eine gute Idee ist. Es
öffnet fünftausend Dateihandles, und dann steht in der Fehlermeldung EMFILE: too many open files.
Wo die Grenze liegt, hängt vom Fall ab. Die Faustregel ist einfacher: Bei einer festen, kleinen Liste
nimm Promise.all oder allSettled ohne nachzudenken. Bei einer Liste, deren Länge von außen kommt,
denk kurz nach. Wie man eine Obergrenze von Hand baut, steht in Lektion 6.6.
Zum Mitnehmen
Ein await in einer Schleife wartet zwanzigmal hintereinander. Promise.all wartet einmal auf alle. Und bei Promise.all reißt ein Fehlschlag alles mit.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.