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
Zeit 2:30–3:20
Der Bon für heute Nacht
#04 · Vierte Nacht
Problem
Wegen einer Gruppenbuchung sind es jetzt sechs Köche. Aber es gibt nur drei Herdplatten, und bevor die Chefin „Offen!“ ruft, darf niemand das Feuer anmachen.
Ziel
Vor dem Öffnungssignal macht niemand Feuer, höchstens drei Herdplatten laufen gleichzeitig, und trotzdem gehen sechs Bestellungen in unter einer Sekunde raus.
Nachtlokal Dongsi
Vierte Nacht, zwanzig vor zehn. Wegen einer Gruppenreservierung sind für einen Tag drei zusätzliche Köche gekommen. Sechs Köche, drei Herdplatten.
Minho stellt sich als Erster an eine Platte und dreht das Feuer auf. „Wenn wir vorheizen, geht's schneller.“ Die Chefin dreht wortlos das Gasventil zu. „Vor dem Öffnen wird kein Feuer gemacht. Und wenn sich sechs Leute vor den Platten drängen, kann sich keiner mehr bewegen.“
Sua hebt die Hand. „Und wenn wir an die Herdplatten nur drei Nummernmarken hängen? Nur wer eine Marke hat, darf ans Feuer.“ Zum ersten Mal lächelt die Chefin. „Das ist das erste Werkzeug heute Nacht.“
Der Lock aus Lektion 3 war ein Stück Kreide, also immer nur eine Person. Ein Semaphore(n) sind n Nummernmarken. Bis zu n Leute gleichzeitig dürfen rein, die Person Nummer n+1 wartet, bis jemand eine Marke zurückgibt.
import threading
import time
stoves = threading.Semaphore(3) # drei Herdplatten
start = time.perf_counter()
def cook(name):
with stoves: # warten, bis eine Platte frei ist, dann zugreifen
print(f"{time.perf_counter() - start:.1f}s {name} on")
time.sleep(0.3)
names = [f"cook{i}" for i in range(1, 7)]
threads = [threading.Thread(target=cook, args=(n,)) for n in names]
for th in threads:
th.start()
for th in threads:
th.join()
print(f"total {time.perf_counter() - start:.2f}s")
# zum Beispiel:
# 0.0s cook1 on
# 0.0s cook2 on
# 0.0s cook3 on
# 0.3s cook4 on
# 0.3s cook5 on
# 0.3s cook6 on
# total 0.60swith stoves: wird eine Marke genommen (acquire), beim Verlassen zurückgegeben (release). Sind null Marken übrig, wartet, wer rein will.Semaphore(1) lässt wie Lock immer nur einen rein. Aber wer bei einem Lock einen schon freigegebenen Lock noch einmal freigibt, bekommt einen RuntimeError; ein Semaphor dagegen zählt bei einem zusätzlichen release() stillschweigend eine Marke dazu (siehe BoundedSemaphore unten).Ändere in der Zeitleiste unten die Zahl der Herdplatten. Gelbe Balken sind Wartezeit auf eine Platte, orange Balken die Zeit, in der wirklich gekocht wird.
Küchen-Zeitleiste
Bestellungen
6
Köche
3
Herdplatten
2
Kochen
Warten
Abgebrochen
Fehlgeschlagen
Ein Semaphore zählt nicht nach, wie viele Marken zurückkommen. Ruft ein Bug release() einmal zu oft auf, sind es heimlich vier Marken. Drei Platten, aber vier Leute am Feuer.
loose = threading.Semaphore(3)
loose.release() # versehentlich einmal zu oft zurückgegeben
print(sum(loose.acquire(blocking=False) for _ in range(10))) # plötzlich vier Platten
# 4
strict = threading.BoundedSemaphore(3)
strict.release()
# ValueError: Semaphore released too many timesBoundedSemaphore wirft einen ValueError, sobald mehr zurückgegeben wird als anfangs festgelegt. Wenn du einen Semaphor zur Begrenzung einer Anzahl verwendest, ist BoundedSemaphore fast immer die bessere Wahl. Der Fehler rutscht nicht still durch, sondern knallt genau an der Stelle.
Tipp der Chefin
Ein Semaphor zählt, er schützt keine Tafel. Auch wenn an drei Platten nur drei Leute stehen: Schreiben die drei an dieselbe Tafel, hast du wieder eine Race Condition. Wenn du in einem Raum, in den bis zu drei reindürfen, einen gemeinsamen Wert anfasst, nimm dort drin noch mal einen Lock.
Ein Event ist eine Signallampe an der Küchenwand. Am Anfang ist sie aus; schaltet sie jemand ein (set()), legen alle, die auf das Signal gewartet haben (wait()), gleichzeitig los.
opened = threading.Event() # am Anfang aus
start = time.perf_counter()
def cook(name):
opened.wait() # warten, bis das Signal angeht
print(f"{time.perf_counter() - start:.1f}s {name} fire")
threads = [threading.Thread(target=cook, args=(n,)) for n in ["minho", "sua", "tak"]]
for th in threads:
th.start()
time.sleep(0.5)
print(opened.is_set())
opened.set() # „Offen!“
for th in threads:
th.join()
print(opened.is_set())
# False
# 0.5s minho fire (die Reihenfolge der drei Zeilen ändert sich von Lauf zu Lauf)
# 0.5s sua fire
# 0.5s tak fire
# TrueDu kannst wait() auch eine Zeitgrenze geben. Dann ist der Rückgabewert wichtig: True, wenn das Signal angegangen ist, False, wenn die Zeit abgelaufen und es immer noch aus ist.
closing = threading.Event()
print(closing.wait(timeout=0.2)) # 0,2 Sekunden gewartet, kein Signal
closing.set()
print(closing.wait(timeout=0.2)) # ist es schon an: sofort True
closing.clear() # wieder ausschalten
print(closing.is_set())
# False
# True
# False| Methode | Was sie tut |
|---|---|
set() | schaltet das Signal ein. Alle wartenden Threads wachen auf |
wait() | wartet, bis das Signal angeht. Ist es schon an, geht es sofort weiter |
wait(timeout) | wartet höchstens so lange. True, wenn es angegangen ist, sonst False |
clear() | schaltet das Signal wieder aus. Folgende wait()-Aufrufe warten wieder |
is_set() | ob es gerade an ist |
Ein Muster kommt sehr oft vor: Man macht aus dem Signal „Feierabend“ ein Event und lässt den arbeitenden Thread bei jedem Schleifendurchlauf mit if closing.is_set(): break nachsehen. So bricht man ihn nicht mittendrin ab wie den daemon-Thread aus Lektion 2, sondern lässt ihn selbst aufräumen und aufhören.
Die Zeitleiste unten zeigt eine Küche, in der die Chefin nach 0,5 Sekunden „Offen!“ ruft.
Küchen-Zeitleiste
Köche
3
Kochen
Warten
Abgebrochen
Fehlgeschlagen
set() weckt alle, die warten.Ein Event kennt nur an oder aus. In der Küche wartet man aber meist auf etwas Konkreteres. Minho wartet auf Eier, Sua auf Tofu. Jedes Mal, wenn eine Zutat ins Regal kommt, muss man nachsehen: „Ist meine dabei?“, und wenn nicht, weiter warten. Dafür gibt es Condition.
Eine Condition ist ein Lock + eine „Weck mich“-Liste. Sie schützt den gemeinsamen Zustand (das Zutatenregal) mit dem Lock, und wenn sich der Zustand ändert, weckt sie die Leute auf der Liste.
pantry = [] # Zutatenregal (gemeinsamer Zustand)
arrived = threading.Condition() # Lock für das Regal + „Weck mich“-Liste
start = time.perf_counter()
def cook(name, need):
with arrived:
ok = arrived.wait_for(lambda: need in pantry, timeout=0.5)
print(f"{time.perf_counter() - start:.1f}s {name} {need if ok else 'gave up'}")
def supplier():
for item in ["tofu", "egg"]: # Fisch kommt heute nicht
time.sleep(0.1)
with arrived:
pantry.append(item)
arrived.notify_all() # alle Wartenden wecken
threads = [threading.Thread(target=cook, args=a) for a in [("minho", "egg"), ("sua", "tofu"), ("tak", "fish")]]
threads.append(threading.Thread(target=supplier))
for th in threads:
th.start()
for th in threads:
th.join()
# 0.1s sua tofu
# 0.2s minho egg
# 0.5s tak gave upwith arrived: den Lock hält. Genau die Regel aus Lektion 3.wait_for(bedingung) wartet, bis die Bedingung wahr wird. Während des Wartens legt es den Lock kurz ab, damit der Lieferant Zutaten ins Regal stellen kann. Beim Aufwachen nimmt es den Lock wieder und prüft die Bedingung erneut.timeout liefert es nach Ablauf der Zeit den letzten Wert der Bedingung. Der Fisch kam nie, also bekam Herr Tak nach 0,5 Sekunden False und gab auf.notify_all() weckt alle auf der Liste. Jeder Geweckte prüft seine eigene Bedingung und schläft wieder ein, wenn sie nicht erfüllt ist. Als der Tofu kam, wurden auch Minho und Herr Tak wach, warteten aber weiter, weil es nicht ihre Zutat war.notify() weckt nur einen Wartenden. Das passt zu Arbeit, bei der es egal ist, wer sie übernimmt, etwa „irgendeine Servicekraft holt einen fertigen Teller ab“.
plates = 0
counter = threading.Condition()
def cook():
global plates
time.sleep(0.1)
with counter:
plates += 1
counter.notify() # nur einen Wartenden wecken
def waiter(name):
global plates
with counter:
counter.wait_for(lambda: plates > 0)
plates -= 1
print(name, "took a plate")
ws = [threading.Thread(target=waiter, args=(n,)) for n in ["w1", "w2"]]
cs = [threading.Thread(target=cook) for _ in range(2)]
for th in ws + cs:
th.start()
for th in ws + cs:
th.join()
print(plates)
# zum Beispiel:
# w1 took a plate
# w2 took a plate
# 0Pro Teller wird nur einer geweckt, also kommt es seltener vor, dass zwei gleichzeitig aufwachen und sich um einen Teller streiten. Wartet dagegen jeder auf eine andere Bedingung, kann notify() den Falschen wecken; dann nimm notify_all(). Im Zweifel ist notify_all() die sichere Wahl.
Tipp der Chefin
Wenn du bei einer Condition wait() benutzt, prüf die Bedingung immer in einer while-Schleife nach. Dass du geweckt wurdest, heißt nicht, dass deine Zutat da ist. Vielleicht war's die von jemand anderem, oder einer hat sie dir schon weggeschnappt. wait_for(bedingung) dreht diese while-Schleife für dich, das ist am bequemsten.
An einem Vierertisch der Gruppe müssen alle vier Teller gleichzeitig rausgehen. Kommt nur einer vorneweg, sitzen die anderen drei da und warten, während sein Essen kalt wird. Barrier(n) lässt alle am Pass warten, bis n Leute angekommen sind, und schickt sie dann zusammen los.
start = time.perf_counter()
table = threading.Barrier(4, action=lambda: print("serve 4 plates"))
done = []
def cook(name, sec):
time.sleep(sec) # jeder hat sein eigenes Tempo
i = table.wait() # am Pass warten, bis alle vier da sind
done.append((round(time.perf_counter() - start, 1), i, name))
crew = [("minho", 0.1), ("sua", 0.2), ("chef", 0.3), ("tak", 0.4)]
threads = [threading.Thread(target=cook, args=c) for c in crew]
for th in threads:
th.start()
for th in threads:
th.join()
print(sorted(done))
# serve 4 plates
# [(0.4, 0, 'minho'), (0.4, 1, 'sua'), (0.4, 2, 'chef'), (0.4, 3, 'tak')]wait() gibt jedem eine andere Nummer von 0 bis n-1. In CPython ist das die Ankunftsreihenfolge: 0, 1, 2, 3. Mit dieser Nummer kannst du festlegen, was „nur einer erledigen soll“ (zum Beispiel if i == 0: einen Logeintrag schreiben).action= läuft genau einmal, wenn der Letzte ankommt. Das ist das Signal, die Teller rauszuschicken.wait(timeout=) die Zeit ab, bricht die Barrier, und alle Wartenden bekommen einen BrokenBarrierError.| Werkzeug | In der Küche | Wann |
|---|---|---|
Lock | ein Stück Kreide | wenn immer nur eine Person einen gemeinsamen Wert ändern darf (Lektion 3) |
Semaphore(n), BoundedSemaphore(n) | n Nummernmarken an den Herdplatten | wenn höchstens n gleichzeitig etwas benutzen dürfen |
Event | Signallampe an der Wand | ein einmaliges Signal wie „Start“ oder „Schluss“ an alle |
Condition | Lock + „Weck mich“-Liste | „Weck mich, wenn diese Bedingung erfüllt ist“: warten, bis sich ein gemeinsamer Zustand ändert |
Barrier(n) | Pass für den Gruppentisch | wenn alle n da sind, geht es gemeinsam los |
Alle fünf stecken in threading, und alle sind auf dem Lock aus Lektion 3 aufgebaut. Tatsächlich verwenden Semaphore, Event und Barrier intern eine Condition, und eine Condition trägt intern einen Lock (wenn du keinen angibst, einen RLock).
Das Ziel für heute Nacht war: Vor dem Öffnungssignal macht niemand Feuer, höchstens drei Herdplatten laufen gleichzeitig, und sechs Bestellungen gehen in unter einer Sekunde raus. Ein Event ist die Startlinie, ein Semaphore(3) regelt die Platten, und mit einem Lock zählen wir, „wie viele gerade am Feuer stehen“.
opened = threading.Event()
stoves = threading.Semaphore(3)
lock = threading.Lock()
on_fire = peak = 0
log = []
start = time.perf_counter()
def cook(n):
global on_fire, peak
opened.wait() # 1. vor „Offen!“ wird kein Feuer gemacht
with stoves: # 2. höchstens drei Platten gleichzeitig
with lock:
on_fire += 1
peak = max(peak, on_fire)
time.sleep(0.3)
with lock:
on_fire -= 1
log.append((round(time.perf_counter() - start, 1), f"#{n}"))
threads = [threading.Thread(target=cook, args=(n,)) for n in range(1, 7)]
for th in threads:
th.start()
time.sleep(0.3) # alles für die Gäste vorbereiten
opened.set()
for th in threads:
th.join()
print(sorted(log))
print(f"peak {peak}, total {time.perf_counter() - start:.2f}s")
# zum Beispiel:
# [(0.6, '#1'), (0.6, '#2'), (0.6, '#3'), (0.9, '#4'), (0.9, '#5'), (0.9, '#6')]
# peak 3, total 0.90speak ist bei jedem Lauf 3. Welche drei Bestellungen bei 0,6 Sekunden rausgehen, kann sich von Lauf zu Lauf ändern.on_fire und peak werden von allen sechs Köchen geändert, deshalb stehen sie in lock. Auch innerhalb des Semaphors.Kurz vor Küchenschluss leert eine Frau, die allein am Fensterplatz sitzt, eine Schüssel Nudeln. Sie schaut einmal auf ihre Armbanduhr, notiert etwas in ein kleines Notizbuch, zahlt leise und geht.
Die Chefin dreht die Ventile der Platten eins nach dem anderen zu. „Heute habt ihr euch alle gut angestellt. Aber was, wenn es außer den Platten auch Messer und Bretter nur je einmal gibt? Wenn Minho mit dem Messer auf das Brett wartet und Sua mit dem Brett auf das Messer, was passiert dann?“
Schichtbuch — Vierte Nacht. 6 Köche, 3 Herdplatten. Vor dem Öffnungssignal (Event) gewartet, Platten mit drei Nummernmarken (Semaphore) begrenzt; 6 Bestellungen in 0,9 Sekunden fertig. Ein Gast am Fensterplatz, eine Schüssel Nudeln.
closing = threading.Event() an und lass einen Koch-Thread alle 0,1 Sekunden eine Schüssel machen, bis closing.is_set() wahr ist. Der Haupt-Thread ruft nach 0,35 Sekunden closing.set() auf und dann join(). Wie viele Schüsseln sind rausgegangen?Barrier(4) durch Barrier(5) ersetzt. Damit es nicht wirklich für immer hängt, ändere es auf table.wait(timeout=1), lass es laufen und sieh nach, welche Ausnahme kommt.Rezeptkarte der Chefin
Semaphore(n): mit with sem: höchstens n gleichzeitig. Geht es um eine Obergrenze, fängt BoundedSemaphore(n) release-Fehler als ValueError ab.
Event: Wird es mit set() eingeschaltet, wachen alle auf, die in wait() hängen. wait(timeout) liefert True, wenn es an ist, sonst False. clear(), is_set().
Condition: in with cond: cond.wait_for(bedingung). Wer den Zustand ändert, ruft notify() (einen) oder notify_all() (alle).
Barrier(n): Mit wait() gehen alle gemeinsam durch, sobald n da sind. Rückgabe-Nummer 0 bis n-1, action= läuft nur einmal.
Auch innerhalb eines Semaphors: Wer gemeinsame Werte ändert, braucht einen Lock. Werkzeuge werden kombiniert.
← Zurück
Lektion 3 · Die Zahl auf der Tafel stimmt nicht: Race Conditions und Lock
Weiter →
Lektion 5 · Messer und Bretter: Deadlock
Ü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.