Nebenläufigkeit in der Nachtküche
·
Kursüberblick
1
Die Schlange am ersten Abend: Nebenläufigkeit und Parallelität
2
Mehr Köche einstellen: Grundlagen von threading
3
Die Zahl auf der Tafel stimmt nicht: Race Conditions und Lock
4
Nur drei Herdplatten: Synchronisationswerkzeuge
5
Messer und Bretter: Deadlock
6
Die Bonschiene: queue
7
Leihköche: concurrent.futures
8
Warum der Teig nicht schneller wird: der GIL und Parallelität
9
Eine Servicechefin für alle Tische: erste Schritte mit asyncio
10
Die Gruppenreservierung platzt: strukturierte Nebenläufigkeit
11
Der Gastraum steht still: blockierende Aufrufe und Kombinieren
12
Die Nacht der Kritikerin: alles zusammen
Der Bon für heute Nacht
#12 · Zwölfte Nacht
Problem
Die Frau am Fenster legt ihre Visitenkarte auf den Tisch: Gastrokritikerin. Heute Nacht bestellt sie ganz offiziell ein Menü mit zwölf Gängen. Der 3., 7. und 11. Teller brauchen beim ersten Versuch auffällig lange.
Ziel
Alle zwölf Teller rausschicken, dabei höchstens drei Herdplatten benutzen und in unter 3 Sekunden fertig sein.
Nachtlokal Dongsi
Freitagnacht, zehn Uhr. Wie versprochen kommt die Frau am Fenster herein. Immer allein, immer eine Schüssel Nudelsuppe, immer Armbanduhr und kleines Notizbuch. Heute legt sie, kaum dass sie sitzt, eine Visitenkarte auf den Tisch. Gastrokritikerin.
„Ich habe diese Küche in den letzten Wochen beobachtet. Heute bestelle ich richtig. Das Menü, zwölf Gänge.“ Die Kritikerin schlägt ihr Notizbuch auf und nimmt die Armbanduhr ab.
In der Küche wird es still. Minho lässt sein Messer fallen, Sua sortiert die Bonschiene noch einmal neu, und Herr Tak wischt einfach seine Herdplatte ab. Lou kommt mit dem Bon herein. Chefin Yoon Seoyeon dreht den Timer an ihrem Hals auf null. „Eine Küche ist ein Programm. Heute lassen wir alles laufen, was wir gebaut haben.“
In elf Nächten haben sich viele Werkzeuge angesammelt. Kommt ein Gast herein, stellst du zuerst eine einzige Frage: „Ist das Arbeit mit Warten oder Arbeit mit vollen Händen?“
| Situation | Werkzeug | In der Küche | Lektion |
|---|---|---|---|
| Sehr viel Warten (Hunderte bis Zehntausende Verbindungen) | asyncio (gather, TaskGroup) | Lou, die allein den Gastraum betreut | 9–10 |
| Warten, aber nur synchrone Bibliotheken, ein paar Dutzend Aufgaben | threading, ThreadPoolExecutor | mehrere Köche, Leihköche | 2, 7 |
| Viel Rechnen (CPU) | ProcessPoolExecutor, multiprocessing (oder Threads unter 3.14t) | Filialküche | 8 |
| Alter blockierender Code innerhalb von asyncio | asyncio.to_thread | eine eigene Person vor dem Ofen | 11 |
| Schwere Berechnung innerhalb von asyncio | loop.run_in_executor(Prozess-Pool, ...) | den Teig an die Filiale geben | 11 |
| Mehrere ändern denselben Wert | Lock, oder per Queue einen einzigen Besitzer bestimmen | Lock an der Tafel, Bonschiene | 3, 6 |
| Höchstens n gleichzeitig | Semaphore(n) | drei Herdplatten | 4, 11 |
| Muss zusammen enden und darf die Zeit nicht überschreiten | TaskGroup, asyncio.timeout | Gruppenreservierung | 10 |
In dieser Reihenfolge wählst du aus.
to_thread.Der Bon der Kritikerin sieht so aus: zwölf Teller, 0,5 Sekunden pro Teller. Aber der 3., 7. und 11. Teller dauern beim ersten Versuch dreimal so lange (1,5 Sekunden), weil die Zutaten noch nicht ganz aufgetaut sind. Beim zweiten Versuch klappt es in 0,5 Sekunden. Herdplatten gibt es nur drei.
Wir brauchen drei Bauteile: eine Semaphore, die die Zahl der Herdplatten einhält, ein Zeitlimit, damit sich kein Teller in die Länge zieht, und Wiederholungen für abgebrochene Teller. Das alles packen wir in einen TaskGroup.
import asyncio, time
SLOW = {3, 7, 11} # Teller, die beim ersten Mal dreimal so lange brauchen
async def plate(n, attempt):
await asyncio.sleep(0.5 * (3 if n in SLOW and attempt == 0 else 1))
async def order(n, stoves, limit_s, retries):
for attempt in range(retries + 1):
async with stoves: # gekocht wird nur, solange man eine Herdplatte hat
try:
async with asyncio.timeout(limit_s):
await plate(n, attempt)
return True
except TimeoutError:
pass # Platte abgeben, hinten anstellen, noch mal
return False
async def service(stoves_n, limit_s, retries):
stoves = asyncio.Semaphore(stoves_n)
start = time.perf_counter()
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(order(n, stoves, limit_s, retries)) for n in range(1, 13)]
served = sum(t.result() for t in tasks)
print(f"stoves={stoves_n} limit={limit_s} retries={retries}: "
f"served {served}/12, {time.perf_counter() - start:.1f}s")
asyncio.run(service(3, None, 0)) # ohne Zeitlimit
asyncio.run(service(3, 0.6, 0)) # nur Zeitlimit
asyncio.run(service(3, 0.6, 1)) # Zeitlimit + eine Wiederholung
# stoves=3 limit=None retries=0: served 12/12, 3.5s
# stoves=3 limit=0.6 retries=0: served 9/12, 2.2s
# stoves=3 limit=0.6 retries=1: served 12/12, 2.7s
# (die Laufzeit schwankt von Lauf zu Lauf um etwa 0,1 s; die erste Zeile zeigt 3.5s oder 3.6s)asyncio.timeout(None) heißt: kein Limit. Deshalb kann dieselbe Funktion sowohl „ohne Limit“ als auch „mit 0,6 Sekunden Limit“ laufen.Am Design sind zwei Punkte wichtig.
async with stoves steht innerhalb des for, also gibt ein abgeschnittener Teller die Platte frei und stellt sich neu an. Würde er mit der Platte in der Hand wiederholen, müssten andere Teller so lange untätig warten.False zurückgegeben. Fliegt in einem TaskGroup eine Exception, wird alles andere abgebrochen (Lektion 10). Bei einer Gruppenreservierung ist das richtig, aber bei einem Menü sollen die übrigen Teller auch dann rausgehen, wenn einer scheitert. Aus demselben Grund könnte man die Aufgaben auch mit asyncio.gather sammeln.In der Praxis wartet man zwischen den Wiederholungen oft etwas länger, etwa mit await asyncio.sleep(0.05 * 2 ** attempt). So verhindert man, dass alle gleichzeitig wieder herandrängen (aus demselben Grund wie beim Livelock in Lektion 5).
Die Zeitleiste unten lässt denselben Prozessor auf dem Server laufen. Eine Zeile ist ein Teller; Versuche, die vom Zeitlimit abgeschnitten wurden, sind grau, Wiederholungen tragen r1, r2. Die Starteinstellung verfehlt das Ziel mit Absicht.
Küchen-Zeitleiste
Bestellungen
6
Herdplatten
2
Zeitlimit (s)
aus
Wiederholungen
0
Kochen
Warten
Abgebrochen
Fehlgeschlagen
Tipp der Chefin
Ein Zeitlimit heißt: ‚länger als ein normaler Teller, kürzer als ein verpatzter‘. Schneidest du schon die normalen Teller ab, helfen dir auch noch so viele Wiederholungen nichts. Bevor du eine Zahl festlegst, miss erst, wie lange es normalerweise dauert.
Im Menü der Kritikerin kommt alles zusammen: Teig (Rechnen), Brot aus dem alten Ofen (blockierend) und Gerichte vom Herd (Warten). Jedes geht an die passende Stelle, und Lou im Gastraum wartet auf alles. Weil Prozesse im Spiel sind, speicherst du den Code als Datei und führst ihn aus.
import asyncio, threading, time
from concurrent.futures import ProcessPoolExecutor
def knead(n): # Teig: Arbeit mit vollen Händen → Filialküche (Prozess)
return sum(i * i for i in range(n)) % 97
tally_lock = threading.Lock() # das Buch, in das Ofen-Thread und Loop beide schreiben
tally = {"plates": 0}
def count_plate():
with tally_lock: # im Lock kein await und nichts Langes
tally["plates"] += 1
def old_oven(item): # der alte Ofen: blockierend → Thread
time.sleep(0.4)
count_plate()
return f"{item} baked"
async def stove_dish(n, stoves): # Gerichte vom Herd: Semaphore
async with stoves:
await asyncio.sleep(0.2)
count_plate()
return f"#{n}"
async def main():
loop = asyncio.get_running_loop()
stoves = asyncio.Semaphore(3)
with ProcessPoolExecutor(max_workers=2) as dough_room:
async with asyncio.timeout(5): # Deadline für das ganze Menü
results = await asyncio.gather(
loop.run_in_executor(dough_room, knead, 3_000_000),
asyncio.to_thread(old_oven, "bread"),
*(stove_dish(n, stoves) for n in range(1, 7)),
)
print(results)
print(tally)
if __name__ == "__main__":
asyncio.run(main())
# [56, 'bread baked', '#1', '#2', '#3', '#4', '#5', '#6']
# {'plates': 7}| Arbeit | Wer sie übernimmt | Werkzeug |
|---|---|---|
| Der ganze Gastraum, das Warten | Lou | asyncio.run, gather |
| Teig (Rechnen) | Filialküche | run_in_executor + ProcessPoolExecutor |
| Alter Ofen (blockierend) | ein Koch vor dem Ofen | asyncio.to_thread |
| Gerichte vom Herd | drei Herdplatten | asyncio.Semaphore(3) |
| Das Buch (gemeinsamer Zustand) | Lock an der Tafel | threading.Lock (weil Thread und Loop es gemeinsam benutzen) |
| Deadline | der Timer der Chefin | asyncio.timeout(5) |
Weil sowohl der Ofen-Thread als auch die Loop ins Buch schreiben, haben wir threading.Lock verwendet. Dass die Loop diesen Lock greifen darf, liegt daran, dass im Lock kein await steht: Es wird nur eine Zahl geändert und sofort wieder freigegeben. Der Unfall aus Lektion 11 passiert, wenn man mit dem Lock in der Hand per await den Platz freigibt.
Damit die Küche vor der Kritikerin nicht stehen bleibt, prüfen wir vor dem Service diese sechs Punkte.
| Prüfpunkt | Symptom | Gegenmittel | Lektion |
|---|---|---|---|
| Gemeinsamer Zustand | Zahlen stimmen manchmal nicht | Lesen bis Schreiben mit Lock zusammenfassen oder per Queue einen einzigen Besitzer bestimmen | 3, 11 |
| Lock-Reihenfolge | Hängt manchmal für immer | Alle sperren in derselben Reihenfolge, acquire(timeout=) | 5 |
| Blockierende Aufrufe | asyncio ist insgesamt langsam | to_thread, mit debug=True aufspüren | 11 |
| Verschluckter Abbruch | Zeitlimit und TaskGroup greifen nicht | CancelledError nach dem Aufräumen wieder mit raise werfen | 10 |
| Tasks ohne Referenz | Hintergrundarbeit verschwindet manchmal | Tasks sammeln und festhalten oder TaskGroup | 9 |
| Queue, die nie fertig wird | join() kehrt nicht zurück | so oft task_done() wie herausgenommen, Endsignal (None, shutdown()) | 6, 11 |
Das Gegenmittel für „Tasks ohne Referenz“ sieht so aus: Tasks in einer Menge festhalten und sie sich selbst austragen lassen, wenn sie fertig sind.
import asyncio
running = set() # hier werden die Tasks festgehalten
async def wipe(table):
await asyncio.sleep(0.05)
print("wiped", table)
async def main():
for t in ("T1", "T2"):
task = asyncio.create_task(wipe(t))
running.add(task)
task.add_done_callback(running.discard) # trägt sich selbst aus, wenn fertig
await asyncio.sleep(0.1)
print(len(running))
asyncio.run(main())
# wiped T1
# wiped T2
# 0Eine „Queue, die nie fertig wird“ entsteht, sobald auch nur ein einziges task_done() fehlt. Hast du den Verdacht, setz ein Zeitlimit auf join() und prüfe.
import asyncio
async def cook(q):
while True:
await q.get()
await asyncio.sleep(0.01)
# q.task_done() ← vergessen
async def main():
q = asyncio.Queue()
for n in range(3):
q.put_nowait(n)
worker = asyncio.create_task(cook(q))
try:
async with asyncio.timeout(0.3):
await q.join()
except TimeoutError:
print("join never finished:", q.qsize(), "in queue")
worker.cancel()
asyncio.run(main())
# join never finished: 0 in queueDie Queue ist leer (0 in queue), aber join() wird nicht fertig. Die drei herausgenommenen Bons haben nie das Zeichen „erledigt“ bekommen.
Wir haben die Küchen der vergangenen Nächte in einem Versuchsaufbau gesammelt. Lass dieselben acht Bestellungen laufen und wechsle nur die Arbeitsweise. Die Regler für Köche und Herdplatten wirken nur in den Modi, die diese Werte verwenden.
Küchen-Zeitleiste
Bestellungen
6
Köche
3
Herdplatten
2
Kochen
Warten
Abgebrochen
Fehlgeschlagen
Die Einstellung, mit der das Menü der Kritikerin klappt, ist 3 Herdplatten, Zeitlimit 0,6 Sekunden, 1 Wiederholung. Diese Werte haben wir mit der Engine des Versuchsaufbaus auf dem Server selbst durchlaufen lassen und geprüft.
| Herdplatten | Zeitlimit | Wiederholungen | Serviert · Fehlgeschlagen | Dauer | Ziel |
|---|---|---|---|---|---|
| 2 | 0,4 s | 0 | 0 · 12 | etwa 2,4 s | gescheitert |
| 3 | aus | 0 | 12 · 0 | etwa 3,5–3,6 s | zu langsam |
| 4 | aus | 0 | 12 · 0 | etwa 3,0 s | zu viele Herdplatten |
| 3 | 0,4 s | 2 | 0 · 12 | etwa 4,9 s | gescheitert |
| 3 | 0,6 s | 0 | 9 · 3 | etwa 2,2 s | 3 Teller gescheitert |
| 3 | 0,6 s | 1 | 12 · 0 | etwa 2,7 s | geschafft |
# mit der Funktion service aus 12-2
asyncio.run(service(3, 0.6, 1))
# stoves=3 limit=0.6 retries=1: served 12/12, 2.7sDer zwölfte Teller geht raus, 2,7 Sekunden. Der Timer der Chefin bleibt stehen. Die Kritikerin schreibt ein paar Zeilen in ihr Notizbuch, leert ihre Nudelschüssel und steht auf. An der Kasse reißt sie eine Seite aus dem Notizbuch und reicht sie der Chefin.
„Zwölf Teller, und kein einziger kam kalt an. Gewartet wurde schon, aber niemand stand untätig herum. Ich komme wieder. Nächstes Mal als ganz normaler Gast.“
Die Kritikerin geht, und in der Küche gehen die Lichter eins nach dem anderen aus. Die Chefin zieht die letzte Rezeptkarte aus ihrer Schürzentasche und reicht sie dir. „Das waren zwölf Nächte. Morgen um zehn ist die Tür auch wieder offen. Diesmal bist du dran, den Bon zu schreiben. Was kommt in die erste Zeile?“
| Lektion | Die Nacht | Was hängen bleibt |
|---|---|---|
| 1 | Die Schlange am ersten Abend: Nebenläufigkeit und Parallelität | Arbeit mit Warten und Arbeit mit vollen Händen auseinanderhalten. Nebenläufigkeit heißt abwechselnd, Parallelität heißt im selben Moment. |
| 2 | Mehr Köche einstellen: Grundlagen von threading | Thread(target=), start(), join(). Ergebnisse sammelst du selbst ein. |
| 3 | Die Zahl auf der Tafel stimmt nicht: Race Conditions und Lock | Drängt sich jemand zwischen Lesen und Schreiben, stimmt die Zahl nicht. with lock: kurz halten. |
| 4 | Nur drei Herdplatten: Synchronisationswerkzeuge | Semaphore, Event, Condition, Barrier. |
| 5 | Messer und Bretter: Deadlock | In derselben Reihenfolge sperren, oder ablegen, wenn es hakt. |
| 6 | Die Bonschiene: queue | Erzeuger und Verbraucher per Queue trennen, mit task_done() und join() wissen, wann Schluss ist. |
| 7 | Leihköche: concurrent.futures | ThreadPoolExecutor, Future, as_completed und map. |
| 8 | Warum der Teig nicht schneller wird: der GIL und Parallelität | Rechnen in Prozesse auslagern, oder 3.14t nehmen. |
| 9 | Eine Servicechefin für alle Tische: erste Schritte mit asyncio | async def, await, gather, create_task. |
| 10 | Die Gruppenreservierung platzt: strukturierte Nebenläufigkeit | TaskGroup, except*, asyncio.timeout; Abbrüche wieder werfen. |
| 11 | Der Gastraum steht still: blockierende Aufrufe und Kombinieren | Blockierendes per to_thread, asyncio-Werkzeuge nur in asyncio. |
Schichtbuch — Zwölfte Nacht. Alle zwölf Teller der Kritikerin mit 3 Herdplatten, 0,6 Sekunden Zeitlimit und 1 Wiederholung in etwa 2,7 Sekunden rausgeschickt. Gastraum mit asyncio, Teig in Prozessen, alter Ofen im Thread, Herdplatten per Semaphore. Ab morgen schreibst du die Bons.
await asyncio.sleep(0.05 * 2 ** attempt)). Solltest du pausieren, bevor du die Herdplatte greifst, oder danach? Lass beides laufen und vergleich die Laufzeit.Rezeptkarte der Chefin
Zuerst fragen: Arbeit mit Warten (asyncio, Threads) oder Arbeit mit vollen Händen (Prozesse, 3.14t)?
Blockierendes in asyncio mit to_thread, schwere Berechnungen mit run_in_executor (Prozess-Pool).
Ressourcenlimit mit Semaphore, Zeitlimit mit asyncio.timeout, was zusammen enden muss, mit TaskGroup.
Wiederholen: Herdplatte abgeben und hinten anstellen. Zeitlimit: ‚länger als normal, kürzer als verpatzt‘.
Checkliste: gemeinsamer Zustand, Lock-Reihenfolge, blockierende Aufrufe, verschluckte Abbrüche, Tasks ohne Referenz, Queues, die nie fertig werden.
Eine Küche ist ein Programm. Zeichne die Balken, dann siehst du es.
Über neue Lektionen informiert werden
Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.
R
ReflexKR
Python und Reflex zum Anfassen. Alle Beispiele wurden mit Reflex 0.10.0 ausgeführt und geprüft.
ReflexKR ist kostenlos und ohne Werbebanner. Wenn es dir geholfen hat, kannst du es mit einem Kaffee unterstützen. ☕ Spendier uns einen Kaffee
Hinweis zu Affiliate-Links© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Fragen & Antworten
Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.