konstante Parallelität statt starrem Batch-Warten
Klassisches wait blockiert, bis alle gestarteten Hintergrundjobs fertig sind, was die Parallelität auf ganze Batches begrenzt. wait -n wartet stattdessen auf den ersten Job, der fertig wird, egal welcher, und macht damit Worker-Pools mit konstant hoher Auslastung erst wirklich praktikabel, ohne auf externe Werkzeuge wie GNU Parallel zurückgreifen zu müssen.
Inhaltsverzeichnis
- 1. Das Problem beim klassischen parallelen Warten
- 2. wait -n Grundlagen: worauf es genau wartet
- 3. Exit-Codes mit wait -n einsammeln
- 4. Worker-Pool-Pattern: konstante Parallelität halten
- 5. wait -n mit PID-Liste: gezielt auf bestimmte Jobs warten
- 6. Timeouts kombinieren: wait -n -t
- 7. Fehlerbehandlung ohne den Rest abzubrechen
- 8. Kompatibilität: Bash-Versionen und Fallbacks
- 9. Job-Harvesting-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem beim klassischen parallelen Warten
Wer in Bash mehrere Hintergrundjobs startet und danach schlicht wait ohne Argument aufruft, blockiert, bis wirklich jeder gestartete Job beendet ist. Das klingt zunächst harmlos, führt aber bei ungleich langen Jobs zu ineffizienter Auslastung: Ein einzelner langsamer Job hält die gesamte Batch-Grenze offen, während schnellere Jobs längst fertig sind und die freigewordene Kapazität ungenutzt bleibt, bis der gesamte Batch durchgelaufen ist.
Das klassische Muster, um Parallelität zu begrenzen, startet Jobs in festen Batches: eine feste Anzahl parallel starten, mit wait auf das ganze Batch warten, nächstes Batch starten. Dieses Muster funktioniert, verschenkt aber Laufzeit, sobald die Jobs innerhalb eines Batches unterschiedlich lange brauchen. wait -n löst genau dieses Problem, indem es zurückkehrt, sobald irgendein Hintergrundjob fertig wird, egal welcher, sodass sofort ein neuer Job nachgestartet werden kann.
Mit wait -n lässt sich damit ein echtes Worker-Pool-Muster in reinem Bash umsetzen, bei dem die Anzahl gleichzeitig laufender Jobs konstant auf einem gewünschten Maximum gehalten wird, ohne auf GNU Parallel oder xargs -P zurückgreifen zu müssen. Die folgenden Abschnitte zeigen die Syntax, das Einsammeln von Exit-Codes und das vollständige Worker-Pool-Pattern im Detail.
2. wait -n Grundlagen: worauf es genau wartet
Das Flag -n wurde in Bash 4.3 zum wait-Builtin hinzugefügt und ändert dessen Verhalten fundamental: Statt auf alle angegebenen Jobs zu warten, kehrt wait -n zurück, sobald der erste von ihnen beendet ist. Ohne weitere Argumente bezieht sich wait -n auf alle aktuell laufenden Hintergrundjobs der Shell, was es zum idealen Baustein für Schleifen macht, die kontinuierlich neue Arbeit nachschieben wollen, sobald Kapazität frei wird.
Der Rückgabewert von wait -n ist der Exit-Code des zuerst beendeten Jobs, nicht automatisch dessen PID. Um herauszufinden, welcher Job das war, muss man entweder mit jobs -l vor und nach dem Aufruf vergleichen, oder, ab Bash 5.1, wait -n mit einer expliziten PID-Liste kombinieren und den Rückgabewert der Shell-Variable prüfen. Dieser Unterschied zwischen "irgendein Job" und "welcher Job genau" ist der zentrale Stolperstein beim ersten Einsatz von wait -n.
#!/usr/bin/env bash
set -euo pipefail
# Start three jobs with different durations
sleep 1 &
sleep 3 &
sleep 5 &
echo "Active jobs: $(jobs -r | wc -l)"
# Classic "wait" blocks until ALL jobs are done (up to 5s here)
# wait
# "wait -n" returns as soon as the FIRST job finishes (after ~1s)
wait -n
echo "First job finished after ~1s, remaining: $(jobs -r | wc -l)"
wait -n
echo "Second job finished after ~3s total, remaining: $(jobs -r | wc -l)"
wait -n
echo "Third job finished after ~5s total, remaining: $(jobs -r | wc -l)"
3. Exit-Codes mit wait -n einsammeln
Der Exit-Code, den wait -n zurückgibt, gehört zu genau dem Job, der als erstes beendet wurde, unabhängig davon, in welcher Reihenfolge die Jobs gestartet wurden. Für Skripte, die wissen müssen, ob alle parallelen Jobs erfolgreich waren, reicht es, den Rückgabewert von wait -n in jeder Iteration zu prüfen und bei einem Fehlercode ungleich null einen Zähler zu erhöhen, statt sofort abzubrechen.
Wichtig ist, wait -n in einer Schleife so oft aufzurufen, wie Jobs gestartet wurden, sonst bleiben unbeendete Jobs als Zombies zurück, bis das Skript selbst endet. Die Anzahl noch laufender Jobs lässt sich jederzeit mit jobs -r | wc -l oder, robuster, mit einem mitgeführten Zähler ermitteln, der bei jedem Start erhöht und bei jedem wait -n verringert wird.
#!/usr/bin/env bash
set -euo pipefail
declare -a exit_codes=()
job_count=0
for url in "${urls[@]}"; do
curl -fsS -o "/dev/null" "$url" &
((job_count++))
done
failed=0
for ((i = 0; i < job_count; i++)); do
if ! wait -n; then
((failed++))
fi
done
echo "Finished: $job_count jobs, failed: $failed"
if (( failed > 0 )); then
exit 1
fi
4. Worker-Pool-Pattern: konstante Parallelität halten
Das eigentliche Kraftstück von wait -n zeigt sich im Worker-Pool-Pattern: Statt in festen Batches zu arbeiten, wird ein neuer Job genau dann gestartet, wenn die Anzahl laufender Jobs unter das Maximum fällt. Dieses Muster hält die Auslastung konstant nahe am gewünschten Parallelitätsgrad, unabhängig davon, wie unterschiedlich lange einzelne Jobs brauchen, was in Batches mit stark schwankenden Laufzeiten einen erheblichen Geschwindigkeitsvorteil gegenüber starrem Batch-Warten bringt.
Die Implementierung braucht dafür nur einen Zähler für aktive Jobs und eine einfache Bedingung: Solange der Zähler das Maximum erreicht hat, wird mit wait -n gewartet, bevor der nächste Job gestartet wird. Dieses Muster ist deutlich kompakter als das klassische Batch-Array-Pattern mit expliziter PID-Verwaltung und liefert gleichzeitig bessere Auslastung.
#!/usr/bin/env bash
set -euo pipefail
MAX_JOBS=4
active=0
failed=0
process_file() {
local file="$1"
gzip -9 "$file"
}
for file in /var/log/archive/*.log; do
process_file "$file" &
((active++))
# Throttle: once at max capacity, wait for ANY job to finish first
if (( active >= MAX_JOBS )); then
wait -n || ((failed++))
((active--))
fi
done
# Drain remaining jobs after the loop
while (( active > 0 )); do
wait -n || ((failed++))
((active--))
done
echo "Done. Failed jobs: $failed"
5. wait -n mit PID-Liste: gezielt auf bestimmte Jobs warten
Ab Bash 5.1 akzeptiert wait -n zusätzlich eine explizite Liste von PIDs oder Jobspecs und wartet dann nur auf das erste Ende innerhalb dieser Teilmenge, statt auf alle Hintergrundjobs der Shell. Das ist besonders nützlich, wenn ein Skript mehrere unabhängige Gruppen von Hintergrundjobs gleichzeitig verwaltet, etwa eine Gruppe für Downloads und eine für Datenbank-Migrationen, die getrennt voneinander überwacht werden sollen.
Mit einer PID-Liste liefert wait -n "${pids[@]}" zusätzlich die PID des beendeten Jobs über die Shell-Variable $! zurück, wenn man vor dem Aufruf die Liste durchsucht, oder man kombiniert dies mit einer Prüfung, welche PID aus der Liste nicht mehr in jobs -r auftaucht. Dieser gezieltere Einsatz von wait -n lohnt sich, sobald mehrere Job-Gruppen unabhängige Fehlerbehandlung benötigen.
#!/usr/bin/env bash
set -euo pipefail
declare -a download_pids=()
declare -a migration_pids=()
for url in "${download_urls[@]}"; do
curl -fsS -O "$url" &
download_pids+=($!)
done
for db in "${migration_dbs[@]}"; do
./migrate.sh "$db" &
migration_pids+=($!)
done
# Bash 5.1+: wait -n on an explicit subset of PIDs
while (( ${#download_pids[@]} > 0 )); do
wait -n "${download_pids[@]}"
# Rebuild the list, dropping PIDs that are no longer running
download_pids=($(jobs -p))
done
6. Timeouts kombinieren: wait -n -t
Bash 5.1 führte zusätzlich das Flag -t für wait ein, das einen Timeout in Sekunden setzt. Kombiniert mit -n wartet wait -n -t Sekunden auf den ersten fertigen Job, gibt aber die Kontrolle zurück, wenn innerhalb der angegebenen Zeit kein Job endet. Der Exit-Code 128 signalisiert dabei speziell, dass der Timeout erreicht wurde, nicht dass ein Job fehlgeschlagen ist, ein Unterschied, den Skripte explizit prüfen müssen.
Dieses Timeout-Verhalten ist wichtig für Worker-Pools, die auch bei hängenden Jobs nicht unbegrenzt blockieren sollen, etwa wenn ein einzelner Netzwerk-Request nie zurückkehrt. Ohne Timeout würde wait -n in so einem Fall den gesamten Fortschritt des Skripts lahmlegen, bis der hängende Job manuell beendet wird.
#!/usr/bin/env bash
set -euo pipefail
MAX_JOBS=4
active=0
for host in "${hosts[@]}"; do
ssh -o ConnectTimeout=5 "$host" "uptime" &
((active++))
if (( active >= MAX_JOBS )); then
# Bash 5.1+: bail out of waiting after 10s even if nothing finished
if wait -n -t 10; then
((active--))
else
status=$?
if (( status == 128 )); then
echo "[WARN] Timeout waiting for a job, checking again" >&2
else
((active--))
fi
fi
fi
done
7. Fehlerbehandlung ohne den Rest abzubrechen
In vielen Batch-Verarbeitungsskripten soll ein einzelner fehlgeschlagener Job nicht das gesamte Skript abbrechen, sondern lediglich vermerkt werden, damit am Ende eine zusammenfassende Fehlermeldung erscheint. wait -n unterstützt dieses Muster direkt, weil sein Rückgabewert pro Job geprüft werden kann, ohne set -e sofort auszulösen, solange der Aufruf in einer if- oder ||-Konstruktion steht.
Für eine saubere Zusammenfassung empfiehlt sich, fehlgeschlagene Jobs mitsamt einer Kennung zu protokollieren, etwa dem Dateinamen oder der URL, die verarbeitet wurde. Da wait -n nur den Exit-Code, nicht automatisch den Kontext des Jobs liefert, muss diese Zuordnung manuell über eine Datenstruktur wie ein assoziatives Array von PID zu Kontext hergestellt werden, wenn detaillierte Fehlerberichte gebraucht werden.
8. Kompatibilität: Bash-Versionen und Fallbacks
wait -n selbst benötigt Bash 4.3, während die PID-Listen-Variante und das -t-Flag erst ab Bash 5.1 verfügbar sind. macOS liefert standardmäßig Bash 3.2 aus Lizenzgründen aus, weshalb wait -n dort ohne eine über Homebrew installierte neuere Bash-Version nicht funktioniert. Skripte, die auf mehreren Plattformen laufen sollen, müssen die Bash-Version zur Laufzeit prüfen, bevor sie wait -n verwenden.
Als Fallback für ältere Bash-Versionen bleibt nur das klassische Batch-Pattern mit einem PID-Array und explizitem Warten auf das älteste Element, wie es auch in Automatisierungsskripten vor Bash 4.3 üblich war. Dieser Fallback ist weniger effizient, aber portabel und funktioniert auch mit der auf macOS vorinstallierten Bash 3.2.
9. Job-Harvesting-Strategien im Vergleich
Die folgende Tabelle vergleicht die verschiedenen Strategien für paralleles Job-Harvesting in Bash.
| Strategie | Auslastung bei ungleichen Laufzeiten | Mindest-Bash-Version | Komplexität |
|---|---|---|---|
| wait ohne Argument (Batch) | Schlecht | Alle Versionen | Niedrig |
| PID-Array + wait "${pids[0]}" | Mittel | Alle Versionen | Mittel |
| wait -n (ohne PID-Liste) | Sehr gut | Bash 4.3+ | Niedrig |
| wait -n mit PID-Liste + -t | Sehr gut | Bash 5.1+ | Mittel |
| GNU Parallel / xargs -P | Sehr gut | Externes Tool | Niedrig, aber Abhängigkeit |
wait -n schließt damit genau die Lücke zwischen dem simplen, aber ineffizienten Batch-Pattern und externen Werkzeugen wie GNU Parallel, die eine zusätzliche Abhängigkeit bedeuten. Für Bash-Skripte ab Version 4.3 ist wait -n in den meisten Fällen die richtige Wahl für effizientes paralleles Job-Harvesting ohne Fremdabhängigkeiten.
Mironsoft
Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur
Parallele Batch-Jobs, die durch starres Warten Zeit verlieren?
Wir bauen Worker-Pool-Muster mit wait -n in eure Bash-Automatisierung ein, mit konstanter Auslastung, sauberer Fehlerbehandlung und Timeout-Absicherung für hängende Jobs, statt starrer Batch-Grenzen.
Parallelisierungs-Audit
Batch-Pattern auf wait -n Potenzial und Auslastung prüfen
Worker-Pool-Design
Konstante Parallelität mit Exit-Code-Sammlung und Timeouts
Kompatibilitäts-Check
Bash-Versionen prüfen und portable Fallbacks einbauen
10. Zusammenfassung
wait -n löst das Kernproblem starrer Batch-Verarbeitung, indem es auf den ersten fertigen Job wartet statt auf alle. Kombiniert mit einem einfachen Zähler entsteht ein Worker-Pool-Pattern, das die Parallelität konstant auf einem gewünschten Maximum hält, unabhängig davon, wie unterschiedlich lange einzelne Jobs brauchen. Der Rückgabewert von wait -n liefert dabei direkt den Exit-Code des jeweils beendeten Jobs.
Ab Bash 5.1 erweitern eine explizite PID-Liste und das -t-Flag wait -n um gezieltes Warten auf Teilmengen und Timeout-Absicherung gegen hängende Jobs. Für ältere Bash-Versionen bleibt das klassische Batch-Pattern als portabler, aber weniger effizienter Fallback. Wer regelmäßig parallele Verarbeitung in Bash-Skripten einsetzt, sollte wait -n als Standardwerkzeug etablieren, statt starrer Batch-Grenzen.
wait -n für paralleles Job Harvesting — Das Wichtigste auf einen Blick
Grundverhalten
wait -n wartet auf den ersten fertigen Hintergrundjob, statt blockierend auf alle. Verfügbar ab Bash 4.3.
Worker-Pool
Mit einem Zähler für aktive Jobs entsteht konstante Parallelität statt starrer Batch-Grenzen.
Timeout & PID-Liste
Ab Bash 5.1: wait -n -t Sekunden für Timeouts, explizite PID-Listen für gezieltes Warten.
Fallback
Für Bash vor 4.3 oder macOS-Standard-Bash: klassisches PID-Array mit Warten auf das älteste Element.