4. Nebenläufigkeit II - Koordination
Nebenläufigkeit II: Koordination
Abschnitt betitelt „Nebenläufigkeit II: Koordination“Mit async/await lassen sich einzelne asynchrone Abläufe schreiben. Reale Anwendungen haben aber selten nur einen: Drei Datenquellen sollen gleichzeitig geladen werden, eine Anfrage soll nach zwei Sekunden aufgeben, ein ungeduldiger Doppelklick darf keine doppelte Bestellung auslösen. Dieses Kapitel behandelt die Koordination mehrerer nebenläufiger Abläufe: die Kombinatoren der Promise-Familie, Zeitsteuerung und Abbruch, die Vermeidung von Race Conditions und schließlich echte Parallelität mit Workern, dem Threading-Modell von JavaScript.
Die Promise-Kombinatoren
Abschnitt betitelt „Die Promise-Kombinatoren“Vier statische Methoden kombinieren mehrere Promises zu einem; sie unterscheiden sich darin, wann das Gesamtergebnis feststeht und was bei Fehlern passiert.
| Kombinator | erfüllt wenn … | scheitert wenn … | typischer Einsatz |
|---|---|---|---|
Promise.all | alle erfüllt sind | eines scheitert | zusammengehörige Daten laden |
Promise.allSettled | alle fertig sind (egal wie) | nie | Sammelaktionen mit Einzelbericht |
Promise.race | das erste fertig ist (egal wie) | das erste scheitert | Timeouts |
Promise.any | das erste erfüllt ist | alle scheitern | schnellste von mehreren Quellen |
Je ein realistischer Fall:
// all: a dashboard needs user, jobs and quota; missing one means broken pageconst [user, jobs, quota] = await Promise.all([ loadUser(id), loadJobs(id), loadQuota(id),]);
// allSettled: send 50 newsletters; one bounce must not stop the restconst results = await Promise.allSettled(recipients.map((r) => sendMail(r)));const failed = results.filter((r) => r.status === "rejected").length;console.log(`${results.length - failed} sent, ${failed} failed`);
// any: three mirror servers, take whichever answers firstconst file = await Promise.any(mirrors.map((url) => download(url)));Die Entscheidung zwischen all und allSettled ist eine fachliche: Ist das Gesamtergebnis ohne einen Teil wertlos (Dashboard), dann all mit gemeinsamem catch. Ist jeder Teil für sich wertvoll (Mailversand), dann allSettled mit Auswertung pro Eintrag. allSettled liefert dafür pro Promise ein Objekt { status: "fulfilled", value } oder { status: "rejected", reason }.
Zeit und Abbruch
Abschnitt betitelt „Zeit und Abbruch“Timeout mit race
Abschnitt betitelt „Timeout mit race“Promise.race lässt zwei Abläufe gegeneinander antreten; mit einem Timer als Gegner entsteht ein Timeout:
function timeout(milliseconds: number): Promise<never> { return new Promise((_, reject) => setTimeout(() => reject(new Error(`timed out after ${milliseconds} ms`)), milliseconds));}
const jobs = await Promise.race([loadJobs(id), timeout(2000)]);Antwortet loadJobs innerhalb von zwei Sekunden, gewinnt es; sonst gewinnt der Timer und die Kette landet im catch. Der Typ Promise<never> sagt präzise: Dieses Promise wird niemals erfüllt, es kann nur scheitern.
Wirklich abbrechen: AbortController
Abschnitt betitelt „Wirklich abbrechen: AbortController“race hat einen Schönheitsfehler: Der Verlierer läuft im Hintergrund weiter und verbraucht Ressourcen. Für echtes Abbrechen gibt es den AbortController, den unter anderem fetch (nächstes Kapitel) direkt unterstützt:
const controller = new AbortController();setTimeout(() => controller.abort(), 2000); // give up after 2 s
try { const response = await fetch(url, { signal: controller.signal });} catch (error) { // on abort: an AbortError lands here}Der Controller besitzt ein signal, das an die Operation übergeben wird; abort() bricht alles ab, was an diesem Signal hängt. Dasselbe Muster bricht auch veraltete Anfragen ab, etwa wenn der Benutzer bei einer Suche weitertippt, bevor die letzte Antwort da ist.
Race Conditions
Abschnitt betitelt „Race Conditions“Eine Race Condition ist ein Fehler, der davon abhängt, in welcher Reihenfolge nebenläufige Abläufe fertig werden. Das greifbarste Beispiel liefert jeder Webshop:
// BUG: double-click means double orderorderButton.addEventListener("click", async () => { await submitOrder(cart); // takes 800 ms; second click fits in here showConfirmation();});Zwischen Klick und Antwort liegen hunderte Millisekunden, und in dieser Lücke passt ein zweiter Klick: Zwei Bestellungen laufen los, beide erfolgreich, der Kunde zahlt doppelt. Der Fehler ist tückisch, weil er beim Testen fast nie auftritt; genau das ist typisch für Race Conditions. Die Standardabwehr ist ein In-Flight-Flag (plus deaktivierter Knopf für die Sichtbarkeit):
let submitting = false;
orderButton.addEventListener("click", async () => { if (submitting) return; // a request is already on its way submitting = true; orderButton.disabled = true; try { await submitOrder(cart); showConfirmation(); } finally { submitting = false; orderButton.disabled = false; }});Die zweite häufige Race Condition ist die überholte Antwort: Bei einer Live-Suche wird nach jedem Buchstaben angefragt, aber die Antworten kommen nicht zwingend in Sendereihenfolge zurück; die Antwort auf “ka” kann die auf “kam” überholen und veraltete Ergebnisse anzeigen. Abwehr: Entweder alte Anfragen per AbortController abbrechen, oder mitzählen und nur die Antwort auf die jüngste Anfrage übernehmen (latest wins).
Merksatz: Sobald zwischen Auslöser und Wirkung eine Wartezeit liegt, muss die Frage gestellt werden: Was passiert, wenn in dieser Lücke noch einmal ausgelöst wird?
Threading: Worker
Abschnitt betitelt „Threading: Worker“Der Event Loop überbrückt Wartezeiten, aber er hilft nicht gegen Rechenzeit: Eine Funktion, die zehn Sekunden rechnet (Bildfilter, Videoanalyse, große Sortierung), blockiert den einen Thread komplett, samt Oberfläche. Für diesen Fall bietet JavaScript Worker: eigenständige Threads mit eigenem Event Loop, im Browser als Web Worker, in Node als Worker Threads. Damit landet das Lehrplan-Thema Threading in der Praxis.
// main.js: start the worker, send work, receive the resultconst worker = new Worker("histogram-worker.js");worker.postMessage({ pixels });worker.addEventListener("message", (event) => { drawHistogram(event.data.histogram);});// histogram-worker.js: runs in its own threadself.addEventListener("message", (event) => { const histogram = computeHistogram(event.data.pixels); // heavy work self.postMessage({ histogram });});Die entscheidende Design-Entscheidung: Worker teilen keinen Speicher mit dem Hauptthread. Kommunikation läuft ausschließlich über Nachrichten (postMessage), deren Daten kopiert werden. Das macht Worker etwas umständlich, erspart JavaScript aber die klassischen Probleme geteilten Speichers, die Threading in anderen Sprachen (Java, C++, Python) berüchtigt machen: Dort können mehrere Threads dieselben Variablen gleichzeitig ändern, und ohne penible Absicherung mit Sperren (Locks) entstehen Wettlauf- und Verklemmungsfehler (Deadlocks), die zu den schwersten Fehlern der Informatik zählen.
Damit ergibt sich eine klare Arbeitsteilung, die auch als Prüfungsantwort taugt:
- Warten (Netzwerk, Dateien, Timer): Event Loop mit
async/await; ein Thread genügt, Worker wären Overhead. - Rechnen (CPU-lastige Arbeit ab spürbarer Dauer): Worker, damit der Hauptthread bedienbar bleibt.
Messen statt glauben
Abschnitt betitelt „Messen statt glauben“Behauptungen über Parallelität sind billig; Zahlen überzeugen. Das Messwerkzeug ist performance.now():
async function measure(label: string, action: () => Promise<unknown>) { const start = performance.now(); await action(); console.log(`${label}: ${Math.round(performance.now() - start)} ms`);}
await measure("sequential", async () => { for (const id of ids) await fetchJobState(id);});await measure("parallel", () => Promise.all(ids.map((id) => fetchJobState(id))));Bei fünf simulierten Anfragen zu je rund 300 Millisekunden liefert das typischerweise etwa 1500 gegen 350 Millisekunden. Solche Messungen gehören zu jeder Parallelisierungs-Entscheidung: Sie belegen den Gewinn, und sie entlarven Fälle, in denen Parallelität nichts bringt (etwa wenn ein einzelner langsamer Teilnehmer dominiert; Promise.all ist immer so langsam wie sein langsamstes Promise).
Lernergebnisse: Was Sie nach diesem Kapitel können sollten
Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:
- Anwenden: mehrere asynchrone Abläufe mit
Promise.all,allSettled,raceundanykoordinieren und die Wahl fachlich begründen. - Anwenden: Timeouts umsetzen und Operationen mit
AbortControllertatsächlich abbrechen. - Analysieren: Race Conditions (Doppelauslösung, überholte Antworten) erkennen und mit In-Flight-Flag, Abbruch oder Latest-wins absichern.
- Erklären: die Arbeitsteilung zwischen Event Loop (Warten) und Workern (Rechnen) erklären und das Nachrichtenmodell der Worker beschreiben.
- Erklären: einordnen, warum Threading mit geteiltem Speicher in anderen Sprachen zusätzliche Absicherung erfordert.
- Anwenden: Laufzeiten messen und Parallelisierungs-Entscheidungen mit Zahlen belegen.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 07 - Parallel oder nacheinander?
- Aufgabe 08 - Der Wettlauf