Zum Inhalt springen
aviral gupta

// B4.3 · ca. 32 Min. · Einstieg

Nacheinander und parallel

Nach dieser Lektion führen Sie asynchrone Operationen nacheinander oder alle zugleich aus, wählen zwischen Promise.all und Promise.allSettled, wenn einige scheitern, und begrenzen, wie viele gleichzeitig laufen.

Lektion 3 von 5 in B4 Asynchrones Node

Danach können Sie

  • Asynchrone Operationen mit await in einer Schleife nacheinander oder mit Promise.all parallel ausführen
  • Fehlschläge in einer Gruppe behandeln: Promise.all, Promise.allSettled und kurz any und race
  • Parallelität mit einem kleinen Pool begrenzen und sie durch Zählen messen, nicht mit der Uhr
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus B4.2: Der Callback ist jetzt async, also ist await darin erlaubt. Was gibt die letzte Zeile aus?

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const done = [];
    
    [30, 10, 20].forEach(async (ms) => {
      await sleep(ms);
      done.push(ms);
    });
    console.log(done);
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. Aufgabe a dauert 60 ms, Aufgabe b 20 ms, und beide gehen in Promise.all. Was gibt das Programm aus?

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    
    async function task(name, ms) {
      console.log('start', name);
      await sleep(ms);
      console.log('end', name);
      return name;
    }
    
    console.log(await Promise.all([task('a', 60), task('b', 20)]));
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie die Methode, die auf jedes Promise wartet, auch nachdem eines abgelehnt wurde, und jedes Ergebnis meldet.

    const results = await Promise.____([
      Promise.resolve('a.txt'),
      Promise.reject(new Error('ENOENT')),
      Promise.resolve('c.txt')
    ]);
    console.log(results.map((result) => result.status));
    const results = await Promise.([
  4. Üben · Aufgabe 4 von 7

    Drei Promises: slow (30 ms) wird erfüllt, fast (10 ms) abgelehnt, medium (20 ms) erfüllt. Ordnen Sie jeder Methode zu, was sie tut.

  5. Üben · Aufgabe 5 von 7

    fail wird nach 10 ms abgelehnt; slow dauert 50 ms. Was gibt das Programm aus?

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    
    const slow = async () => {
      await sleep(50);
      console.log('slow done');
    };
    const fail = async () => {
      await sleep(10);
      throw new Error('boom');
    };
    
    try {
      await Promise.all([slow(), fail()]);
    } catch (error) {
      console.log('caught', error.message);
    }
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Zwei Worker teilen sich eine Liste mit vier Aufgaben: a dauert 100 ms, b, c und d je 20 ms. In welcher Reihenfolge enden die Aufgaben?

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const tasks = [['a', 100], ['b', 20], ['c', 20], ['d', 20]];
    let next = 0;
    
    async function worker() {
      while (next < tasks.length) {
        const [name, ms] = tasks[next++];
        await sleep(ms);
        console.log('end', name);
      }
    }
    
    await Promise.all([worker(), worker()]);
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Listen Sie in main.js die .txt-Dateien des Ordners notes mit readdir aus node:fs/promises auf, nach Namen sortiert. Zählen Sie die Wörter jeder Datei mit höchstens zwei Lesevorgängen zugleich, über einen selbst geschriebenen Pool, und geben Sie eine Zeile je Datei aus, name: N words, in Namensreihenfolge, danach die Summe. Legen Sie drei oder vier kleine .txt-Dateien in notes und führen Sie node main.js aus.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Drei Dateien nacheinander, parallel und in einem Pool lesen

read() liest eine Datei und zählt, wie viele Lesevorgänge im selben Moment laufen; most behält den höchsten Wert. Das Programm liest dieselben drei Dateien nacheinander, alle zugleich mit Promise.all und mit einem Pool aus zwei. Dann liest es eine Liste mit einer fehlenden Datei, zuerst mit Promise.allSettled, das jede Datei meldet, dann mit Promise.all, das abgelehnt wird. Zählungen und Reihenfolge sind bei jedem Lauf gleich; die Zeiten wären es nicht.

main.js

import {readFile} from 'node:fs/promises';

let active = 0; // reads running right now
let most = 0; // the most that ran at the same time

// Reads one file, and counts how many reads run at once.
async function read(name) {
  active++;
  most = Math.max(most, active);
  try {
    return (await readFile(name, 'utf8')).trim();
  } finally {
    active--;
  }
}

// At most limit calls of fn at once; results in input order.
async function mapLimit(items, limit, fn) {
  const results = [];
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const index = next++;
      results[index] = await fn(items[index]);
    }
  }
  await Promise.all(Array.from({length: limit}, worker));
  return results;
}

const names = ['a.txt', 'b.txt', 'c.txt'];

most = 0;
const one = [];
for (const name of names) one.push(await read(name));
console.log('in sequence:', one, '| at most', most, 'at once');

most = 0;
const all = await Promise.all(names.map(read));
console.log('in parallel:', all, '| at most', most, 'at once');

most = 0;
const pooled = await mapLimit(names, 2, read);
console.log('pool of 2:', pooled, '| at most', most, 'at once');

const withMissing = ['a.txt', 'missing.txt', 'c.txt'];
const settled = await Promise.allSettled(withMissing.map(read));
settled.forEach((result, i) => {
  console.log(withMissing[i], result.status, result.value ?? result.reason.code);
});

try {
  await Promise.all(withMissing.map(read));
} catch (error) {
  console.log('Promise.all rejected:', error.code);
}

a.txt

alpha

b.txt

beta

c.txt

gamma

Ausführen mit

node main.js

Ausgabe

in sequence: [ 'alpha', 'beta', 'gamma' ] | at most 1 at once
in parallel: [ 'alpha', 'beta', 'gamma' ] | at most 3 at once
pool of 2: [ 'alpha', 'beta', 'gamma' ] | at most 2 at once
a.txt fulfilled alpha
missing.txt rejected ENOENT
c.txt fulfilled gamma
Promise.all rejected: ENOENT
  • Die Schleife wartet jedes Lesen ab, bevor sie das nächste startet, also läuft nie mehr als eines.
  • map ruft read dreimal auf, bevor irgendetwas abgewartet wird, also laufen alle drei zugleich.
  • Array.from({length: 2}, worker) startet zwei Worker; sie teilen sich next, also wird jede Datei einmal gelesen.
  • Alle drei Wege liefern die Texte in der Reihenfolge von names.
  • allSettled meldet die zwei guten Dateien und das ENOENT; Promise.all liefert nur den Fehler.

Übungen

Übung 1 von 2

Ein Pool mit Limit

mapLimit(items, limit, fn) ruft fn(item) für jedes Element auf und gibt die Ergebnisse in der Reihenfolge von items zurück. Der Starter startet jeden Aufruf sofort. Ändern Sie ihn so, dass höchstens limit Aufrufe gleichzeitig laufen: Starten Sie limit Worker (oder weniger, wenn es weniger Elemente gibt), und lassen Sie jeden Worker den nächsten Index nehmen, bis keiner übrig ist. Dieser Teil läuft auch im Browser: slowDouble steht für ein langsames Lesen.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Halten Sie let next = 0; und ein results-Array außerhalb der Worker. Ein Worker läuft, solange next < items.length gilt, und nimmt const index = next++.

  2. Hinweis 2

    Speichern Sie das Ergebnis mit results[index] = await fn(items[index]); so folgt die Reihenfolge items, nicht dem Fertigwerden.

  3. Hinweis 3

    Starten Sie Math.min(limit, items.length) Worker in einem Array, warten Sie dann await Promise.all(workers) ab und geben Sie results zurück.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// Calls fn(item) for every item, with at most limit calls running at once.
// The results come back in the order of items, not in the order they finish.
export async function mapLimit(items, limit, fn) {
  const results = [];
  let next = 0; // the index of the next item to start
  async function worker() {
    while (next < items.length) {
      const index = next++;
      results[index] = await fn(items[index]);
    }
  }
  const workers = [];
  for (let i = 0; i < Math.min(limit, items.length); i++) workers.push(worker());
  await Promise.all(workers);
  return results;
}

// A stand-in for slow work: waits ms milliseconds, then doubles ms.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let active = 0;
let most = 0;
const slowDouble = async (ms) => {
  active++;
  most = Math.max(most, active);
  await sleep(ms);
  active--;
  return ms * 2;
};

console.log(await mapLimit([30, 10, 20, 10], 2, slowDouble));
console.log('at most', most, 'at once');
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// Calls fn(item) for every item, with at most limit calls running at once.
// The results come back in the order of items, not in the order they finish.
export async function mapLimit(items, limit, fn) {
  return Promise.all(items.map(fn)); // starts every call at once
}

// A stand-in for slow work: waits ms milliseconds, then doubles ms.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let active = 0;
let most = 0;
const slowDouble = async (ms) => {
  active++;
  most = Math.max(most, active);
  await sleep(ms);
  active--;
  return ms * 2;
};

console.log(await mapLimit([30, 10, 20, 10], 2, slowDouble));
console.log('at most', most, 'at once');

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {mapLimit} from './main.js';

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// fn for the tests: counts the calls running at once, and keeps the highest count.
function counter() {
  const seen = {active: 0, most: 0, calls: 0};
  seen.fn = async (ms) => {
    seen.calls++;
    seen.active++;
    seen.most = Math.max(seen.most, seen.active);
    await sleep(ms);
    seen.active--;
    return ms * 2;
  };
  return seen;
}

test('gibt die Ergebnisse in der Reihenfolge von items zurück', async () => {
  const got = await mapLimit([30, 10, 20], 2, counter().fn);
  assert.deepEqual(got, [60, 20, 40], `mapLimit gab ${JSON.stringify(got)} zurück`);
});

test('führt nie mehr als limit Aufrufe zugleich aus', async () => {
  const seen = counter();
  await mapLimit([20, 10, 30, 10, 20, 10], 2, seen.fn);
  assert.equal(seen.most, 2, `höchstens ${seen.most} Aufrufe liefen zugleich; erwartet waren 2`);
});

test('ruft fn einmal je Element auf, auch wenn limit größer als die Liste ist', async () => {
  const seen = counter();
  const got = await mapLimit([10, 20], 5, seen.fn);
  assert.deepEqual(got, [20, 40], `mapLimit gab ${JSON.stringify(got)} zurück`);
  assert.equal(seen.calls, 2, `fn wurde ${seen.calls}-mal aufgerufen; erwartet war 2-mal`);
});

test('eine leere Liste ergibt ein leeres Array', async () => {
  const got = await mapLimit([], 3, counter().fn);
  assert.deepEqual(got, [], `mapLimit gab ${JSON.stringify(got)} zurück`);
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 2 von 2

Ein Größenbericht, der eine fehlende Datei übersteht

sizeReport(paths, statFile) gibt eine Zeile je Pfad zurück, in der Reihenfolge von paths: 'a.txt: 5 bytes' aus der Größe der Datei oder 'missing.txt: ENOENT' aus dem code des Fehlers, wenn es scheitert. statFile ist stat aus node:fs/promises; die Tests übergeben einen Ersatz, der Aufrufe zählt. Der Starter ruft stat für eine Datei nach der anderen auf und bricht beim ersten Fehler ab. Starten Sie alle stat-Aufrufe zugleich und melden Sie jede Datei mit Promise.allSettled. Führen Sie node main.js aus, dann node --test.

Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    paths.map((path) => statFile(path)) startet jeden stat-Aufruf sofort und liefert ein Array von Promises.

  2. Hinweis 2

    await Promise.allSettled(promises) wird nie abgelehnt; jedes Ergebnis hat status und value oder reason.

  3. Hinweis 3

    Machen Sie aus jedem Ergebnis eine Zeile: result.value.size bei 'fulfilled', result.reason.code bei 'rejected'. Mit dem Index i bekommen Sie paths[i].

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import {stat} from 'node:fs/promises';

// One line per path: 'name: N bytes', or 'name: CODE' when stat fails.
export async function sizeReport(paths, statFile = stat) {
  const results = await Promise.allSettled(paths.map((path) => statFile(path)));
  return results.map((result, i) =>
    result.status === 'fulfilled' ? paths[i] + ': ' + result.value.size + ' bytes' : paths[i] + ': ' + result.reason.code
  );
}

console.log(await sizeReport(['a.txt', 'missing.txt', 'b.txt']).catch((error) => error.message));
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

import {stat} from 'node:fs/promises';

// One line per path: 'name: N bytes', or 'name: CODE' when stat fails.
export async function sizeReport(paths, statFile = stat) {
  const lines = [];
  for (const path of paths) {
    const info = await statFile(path);
    lines.push(path + ': ' + info.size + ' bytes');
  }
  return lines;
}

console.log(await sizeReport(['a.txt', 'missing.txt', 'b.txt']).catch((error) => error.message));

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {sizeReport} from './main.js';

test('meldet die Größe jeder Datei', async () => {
  const got = await sizeReport(['a.txt', 'b.txt']);
  assert.deepEqual(got, ['a.txt: 5 bytes', 'b.txt: 3 bytes'], `sizeReport gab ${JSON.stringify(got)} zurück`);
});

test('eine fehlende Datei wird mit ihrem code gemeldet, die anderen trotzdem', async () => {
  const got = await sizeReport(['a.txt', 'missing.txt', 'b.txt']);
  assert.deepEqual(got, ['a.txt: 5 bytes', 'missing.txt: ENOENT', 'b.txt: 3 bytes'], `sizeReport gab ${JSON.stringify(got)} zurück`);
});

test('jeder stat-Aufruf startet, bevor der erste fertig ist', async () => {
  let active = 0;
  let most = 0;
  const fakeStat = async (path) => {
    active++;
    most = Math.max(most, active);
    await new Promise((resolve) => setTimeout(resolve, 10));
    active--;
    return {size: path.length};
  };
  const got = await sizeReport(['x', 'yy', 'zzz', 'w'], fakeStat);
  assert.deepEqual(got, ['x: 1 bytes', 'yy: 2 bytes', 'zzz: 3 bytes', 'w: 1 bytes'], `sizeReport gab ${JSON.stringify(got)} zurück`);
  assert.equal(most, 4, `höchstens ${most} stat-Aufrufe liefen zugleich; erwartet waren alle 4`);
});

a.txt

hello

b.txt

hi

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Häufige Fehler

Ergebnisse in forEach sammeln

import {stat} from 'node:fs/promises';

const sizes = [];
['a.txt', 'b.txt'].forEach(async (name) => {
  sizes.push((await stat(name)).size);
});
console.log(sizes[0].toFixed(0));

Was Node.js ausgibt

TypeError: Cannot read properties of undefined (reading 'toFixed')

Warum, und die Lösung

forEach ignoriert die Promises, die sein async-Callback zurückgibt, also wurde noch nichts hinzugefügt, wenn die nächste Zeile läuft: sizes[0] ist undefined. Machen Sie das Warten ausdrücklich: const sizes = await Promise.all(names.map(async (name) => (await stat(name)).size)); für paralleles Lesen, oder eine for...of-Schleife mit await für eins nach dem anderen.

Promise.all, wenn Sie jedes Ergebnis brauchen

import {readFile} from 'node:fs/promises';

const names = ['a.txt', 'missing.txt', 'b.txt'];
const texts = await Promise.all(names.map((name) => readFile(name, 'utf8')));
console.log(texts.length, 'files read');

Was Node.js ausgibt

Error: ENOENT: no such file or directory, open 'missing.txt'

Warum, und die Lösung

Eine fehlende Datei lässt das ganze Promise.all scheitern, und die zwei gelesenen Texte gehen verloren. Soll ein Fehlschlag alles stoppen, ist das richtig, aber fangen Sie ihn ab. Wollen Sie jede Datei melden, nehmen Sie Promise.allSettled und sehen Sie sich jedes result.status an: fulfilled liefert value, rejected liefert reason.

Die Promises einzeln übergeben

const first = Promise.resolve('a');
const second = Promise.resolve('b');

console.log(await Promise.all(first, second));

Was Node.js ausgibt

TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

Warum, und die Lösung

Promise.all, allSettled, any und race nehmen ein Argument: ein Iterable, meist ein Array, von Promises. Hier bekommt Promise.all das Promise first als Argument, das nicht iterierbar ist, also wird es mit einem TypeError abgelehnt, und second wird ignoriert. Packen Sie beide in ein Array: await Promise.all([first, second]).

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Eins nach dem anderen, oder alle zugleich

for (const name of names) { texts.push(await readFile(name, 'utf8')); } liest eine Datei, wartet auf sie und startet dann die nächste: nacheinander. Die Gesamtzeit ist die Summe aller Lesevorgänge. Nutzen Sie das, wenn ein Schritt das Ergebnis des vorigen braucht oder die Reihenfolge der Nebenwirkungen zählt. names.map((name) => readFile(name, 'utf8')) startet jedes Lesen sofort und liefert ein Array von Promises; await Promise.all(promises) wartet auf alle: parallel. Die Gesamtzeit ist etwa die des langsamsten. Die Ergebnisse von Promise.all stehen in der Reihenfolge der Eingabe, nicht in der, in der das Lesen fertig wird. Ein forEach mit async-Callback startet alles und wartet auf nichts.

Wenn einige scheitern

Promise.all wird abgelehnt, sobald eines seiner Promises abgelehnt wird, mit diesem ersten Fehler; die Werte, die gelungen sind, gehen verloren. Die anderen Operationen werden nicht gestoppt: Sie laufen weiter, und ihre Ergebnisse werden verworfen. Wenn Sie jedes Ergebnis melden wollen, nehmen Sie Promise.allSettled. Es wird nie abgelehnt; es erfüllt sich mit einem Objekt je Eingabe, {status: "fulfilled", value} oder {status: "rejected", reason}, in Eingabereihenfolge. Zwei weitere, kurz: Promise.any erfüllt sich mit dem ersten Wert, der gelingt, und wird nur mit einem AggregateError abgelehnt, wenn alle abgelehnt werden; Promise.race wird entschieden wie das erste Promise, das entschieden wird, Erfolg oder Fehlschlag.

Höchstens N zugleich, und wie Sie das prüfen

Tausend Lesevorgänge auf einmal können Speicher oder offene Dateien erschöpfen. Ein Pool begrenzt das: Starten Sie limit Worker; jeder nimmt in einer Schleife mit const index = next++ das nächste Element, bis keines übrig ist. JavaScript führt immer nur ein Stück Code zugleich aus, also nehmen zwei Worker nie denselben Index. Stapel sind einfacher, await Promise.all über items.slice(i, i + limit), aber jeder Stapel wartet auf sein langsamstes Element. Zum Prüfen eines Pools vergleichen Sie keine Uhrzeiten, die schwanken: Zählen Sie. Erhöhen Sie active vor jedem await, senken Sie es danach, merken Sie sich den höchsten Wert und vergleichen Sie ihn mit dem Limit. Vergleichen Sie Reihenfolgen, keine Millisekunden; wenn Sie Zeit messen müssen, prüfen Sie einen Bereich.

Quellen

Zuletzt geprüft am 4. Oktober 2026