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 0:50–1:40
Der Bon für heute Nacht
#02 · Zweite Nacht
Problem
Minho, Sua und Herr Tak haben ihren ersten Arbeitstag. Alle drei wirbeln herum, aber niemand weiß, wer wann was fertig gemacht hat.
Ziel
Sechs Bestellungen auf drei Leute verteilen, in unter 0,6 Sekunden fertig sein und festhalten, wer nach wie vielen Sekunden was fertig hatte.
Nachtlokal Dongsi
Zweite Nacht, halb zehn. Die drei Postenköche binden ihre Schürzen. Minho hat schon das Messer in der Hand, Sua sortiert die Bons der Reihe nach, und Herr Tak wischt in aller Ruhe die Herdplatten ab.
Um zehn kommen die Bestellungen, und jeder setzt seinen Topf auf. Doch eine halbe Stunde später fragt die Chefin: „Wer hat die Udon für Tisch 3 gemacht?“, und niemand kann antworten.
Die Chefin tippt auf den Timer an ihrem Hals. „Eingestellt haben wir die Köche. Heute lernen wir, wie man ihnen Namensschilder gibt, Arbeit zuteilt und wartet, bis sie fertig sind.“
In Python ist ein Koch ein Thread. Alle Threads teilen sich dieselbe Küche (den Speicher), aber jeder erledigt seine eigene Arbeit. Threads erzeugst du mit threading.Thread.
import threading
import time
def cook(name, dish):
print(f"{name}: start {dish}")
time.sleep(0.3) # warten, bis die Suppe kocht
print(f"{name}: done {dish}")
minho = threading.Thread(target=cook, args=("minho", "ramen"))
minho.start() # Minho fängt an
print("chef: next order") # die Chefin wartet nicht, sondern macht weiter
minho.join() # warten, bis Minho fertig ist
print("chef: served")
# minho: start ramen
# chef: next order
# minho: done ramen
# chef: servedtarget= bekommt die Arbeit (die Funktion), die der Thread erledigen soll, und args= die Argumente für diese Funktion, und zwar als Tupel. Du rufst die Funktion nicht auf, sondern übergibst sie selbst, also cook und nicht cook().Thread(...) anzulegen bewirkt noch gar nichts. Der Koch ist nur eingestellt. Erst wenn du start() aufrufst, läuft cook in einem neuen Thread los.start() kehrt sofort zurück. Während Minho also Ramen kocht, geht die Chefin (der Haupt-Thread, der das Programm gestartet hat) schon zur nächsten Zeile weiter.join() wartet, bis dieser Thread fertig ist. Sozusagen: „Am Pass warten, bis Minho den Teller herüberreicht.“Bekommt jeder der drei eine Bestellung, sieht das so aus:
def cook(name, dish):
time.sleep(0.3)
print(f"{name}: done {dish}")
crew = [("minho", "ramen"), ("sua", "udon"), ("tak", "soba")]
threads = [threading.Thread(target=cook, args=c) for c in crew]
start = time.perf_counter()
for th in threads:
th.start() # erst alle drei losschicken
for th in threads:
th.join() # dann auf alle warten
print(f"{time.perf_counter() - start:.2f}s")
# zum Beispiel:
# sua: done udon
# minho: done ramen
# tak: done soba
# 0.30sIn welcher Reihenfolge die drei fertig werden, ändert sich bei jedem Lauf. Alle drei sind nach 0,3 Sekunden fast gleichzeitig fertig, und es hängt vom Betriebssystem ab, wen es zuerst aufweckt. In nebenläufigem Code ist die „Ausgabereihenfolge“ kein Versprechen.
start() und join() gehören nicht in dieselbe Schleife. Wechselst du th.start(); th.join() ab, startet der Nächste erst, wenn der Vorige fertig ist, und am Ende dauert es 0,9 Sekunden.
Du kannst dem Warten auch eine Grenze setzen. join(timeout=sekunden) wartet nur so lange und kehrt dann zurück; ob der Thread noch arbeitet, prüfst du mit is_alive().
slow = threading.Thread(target=time.sleep, args=(1.0,))
slow.start()
slow.join(timeout=0.2)
print(slow.is_alive()) # True: kocht noch
slow.join()
print(slow.is_alive()) # FalseKüchen-Zeitleiste
Bestellungen
6
Köche
3
Kochen
Warten
Abgebrochen
Fehlgeschlagen
Thread an und rufen start() und join() auf.Thread hat auch eine Methode namens run(). Dem Namen nach heißt sie „ausführen“, aber wer sie direkt aufruft, tappt in eine Falle.
def cook(name, dish):
time.sleep(0.3)
print(f"{threading.current_thread().name}: done {dish}")
threads = [threading.Thread(target=cook, args=c) for c in crew]
start = time.perf_counter()
for th in threads:
th.run() # run() statt start()
print(f"{time.perf_counter() - start:.2f}s")
# MainThread: done ramen
# MainThread: done udon
# MainThread: done soba
# 0.90srun() ist eine ganz normale Methode, die target aufruft. Sie erzeugt keinen neuen Thread, sondern führt alles der Reihe nach im aktuellen Thread aus. Deshalb hat jedes Mal MainThread gekocht, und es dauert 0,3 × 3 = 0,9 Sekunden.start(). start() erzeugt den neuen Thread und ruft darin run() auf.Ein Thread kann nur einmal starten. Rufst du start() zweimal für denselben Thread auf, gibt es einen Fehler. Für dieselbe Arbeit noch einmal legst du einen neuen Thread an.
th = threading.Thread(target=time.sleep, args=(0.1,))
th.start()
th.start()
# RuntimeError: threads can only be started onceTipp der Chefin
In args=("ramen") fehlt das Komma, also ist das kein Tupel, sondern einfach ein String. Dann werden die fünf Buchstaben r, a, m, e, n einzeln übergeben, und es knallt mit 'cook() takes 1 positional argument but 5 were given'. Bei nur einem Argument immer das Komma setzen: args=("ramen",). Und noch was: Eine Ausnahme in einem Thread kommt nicht bei der Chefin an. Auf dem Bildschirm erscheinen nur 'Exception in thread ...' und ein Traceback, und das Programm läuft einfach weiter. Schau dir die Logs an, damit dir so was nicht durchrutscht.
Das Problem heute Nacht war: „Keiner weiß, wer es war.“ Threads kann man ein Namensschild geben.
def cook(dish):
me = threading.current_thread() # der Thread, der diesen Code gerade ausführt
print(me.name, dish, threading.get_native_id())
time.sleep(0.2)
a = threading.Thread(target=cook, args=("ramen",), name="minho")
b = threading.Thread(target=cook, args=("udon",)) # und ohne Namen?
a.start(); b.start()
print([t.name for t in threading.enumerate()])
print(threading.active_count())
a.join(); b.join()
print(threading.current_thread() is threading.main_thread())
print([t.name for t in threading.enumerate()])
# zum Beispiel:
# minho ramen 25680
# Thread-1 (cook) udon 38072
# ['MainThread', 'minho', 'Thread-1 (cook)']
# 3
# True
# ['MainThread']| Werkzeug | Liefert |
|---|---|
Thread(name="minho") | gibt dem Thread einen Namen. Ohne Angabe bekommt er eine Nummer und den Funktionsnamen, etwa Thread-1 (cook) |
threading.current_thread() | das Thread-Objekt, das gerade diese Zeile ausführt |
threading.main_thread() | den Haupt-Thread, der das Programm gestartet hat (Name MainThread). In unserer Küche: die Chefin |
threading.enumerate() | die Liste der Threads, die gerade leben (inklusive Haupt-Thread) |
threading.active_count() | die Länge dieser Liste |
threading.get_native_id() | die Thread-Nummer, die das Betriebssystem vergeben hat. Dieselbe Nummer, die Systemwerkzeuge wie top -H unter Linux anzeigen |
b ohne Namen Thread-1 heißt und nicht Thread-2, liegt daran, dass nur Threads ohne Namen durchnummeriert werden.get_native_id() ist bei jedem Lauf anders. Die Nummer, die Python intern verwendet, ist th.ident (oder threading.get_ident()); beide stehen erst fest, wenn der Thread gestartet ist.enumerate(). Wer Feierabend hat, steht nicht mehr auf der Liste.Eine Funktion liefert ihr Ergebnis mit return, aber ein Thread nimmt diesen Wert nicht entgegen. Der Rückgabewert der target-Funktion wird einfach verworfen, und auch join() liefert nichts zurück.
def make(dish):
time.sleep(0.1)
return f"{dish} ok"
th = threading.Thread(target=make, args=("ramen",))
th.start()
print(th.join()) # join liefert nichts zurück
# NoneAlso stellst du vorher ein Gefäß für die Ergebnisse bereit, in das der Thread sie legt. Am einfachsten bekommt jeder Koch sein eigenes Fach.
dishes = ["ramen", "udon", "soba"]
results = [None] * len(dishes) # die Plätze vorher anlegen
def cook(i, dish):
results[i] = make(dish) # nur ins eigene Fach schreiben
threads = [threading.Thread(target=cook, args=(i, d)) for i, d in enumerate(dishes)]
for th in threads:
th.start()
for th in threads:
th.join()
print(results)
# ['ramen ok', 'udon ok', 'soba ok']Willst du nach Namen nachschlagen, nimm ein dict.
done = {}
def cook2(name, dish):
done[name] = make(dish)
ts = [threading.Thread(target=cook2, args=(n, d)) for n, d in [("minho", "ramen"), ("sua", "udon")]]
for t in ts:
t.start()
for t in ts:
t.join()
print(sorted(done.items()))
# [('minho', 'ramen ok'), ('sua', 'udon ok')]join() durch ist. Vorher kann noch ein Fach auf None stehen.Future, den ThreadPoolExecutor aus Lektion 7 zurückgibt, nimmt dir das ab. Mit future.result() bekommst du den Teller direkt.Ein Python-Programm beendet sich nicht schon, wenn der Haupt-Thread fertig ist, sondern wartet, bis alle Nicht-daemon-Threads fertig sind. Normal angelegte Threads sind alle keine daemon-Threads.
def clean():
for i in range(1, 6):
print("cleaning", i)
time.sleep(0.1)
th = threading.Thread(target=clean) # daemon=False (Standard)
th.start()
time.sleep(0.25)
print("lights off")
# cleaning 1
# cleaning 2
# cleaning 3
# lights off
# cleaning 4
# cleaning 5Auch wenn die Chefin „Licht aus!“ ruft, wird die Tür erst abgeschlossen, wenn der Koch mit dem Putzen fertig ist. Hängen wir an denselben Code nur daemon=True an:
th = threading.Thread(target=clean, daemon=True)
th.start()
time.sleep(0.25)
print("lights off", th.daemon)
# cleaning 1
# cleaning 2
# cleaning 3
# lights off Truejoin() auf. join() wartet unabhängig davon, ob ein Thread daemon ist.start() fest. Gibst du nichts an, erbt er den Wert des Threads, der ihn erzeugt hat; der Haupt-Thread ist kein daemon, also ist es meistens False.Von Thread erben und run ersetzen. Das bietet sich an, wenn ein Koch seinen eigenen Zustand (zugeteiltes Gericht, Ergebnis) mit sich tragen soll. Wie in 2-2 gesehen, ruft start() in einem neuen Thread run() auf, also musst du nur run() neu schreiben.
class Cook(threading.Thread):
def __init__(self, name, dish):
super().__init__(name=name) # unbedingt zuerst das __init__ der Elternklasse
self.dish = dish
self.result = None
def run(self): # start() ruft das in einem neuen Thread auf
time.sleep(0.1)
self.result = f"{self.dish} ok"
tak = Cook("tak", "soba")
tak.start()
tak.join()
print(tak.name, tak.result)
# tak soba okthreading.Timer: einmal nach n Sekunden. Wie der Nudeltimer: ein Thread, der nach einer festgelegten Zeit einmal eine Funktion aufruft. Solange er noch nicht geklingelt hat, kannst du ihn mit cancel() abstellen.
start = time.perf_counter()
def ring(msg):
print(f"{time.perf_counter() - start:.1f}s", msg)
noodles = threading.Timer(0.5, ring, args=("noodles up",))
noodles.start() # klingelt einmal nach 0,5 Sekunden
eggs = threading.Timer(1.0, ring, args=("eggs up",))
eggs.start()
eggs.cancel() # vorher abgestellt, passiert gar nichts
noodles.join()
eggs.join()
print("end")
# 0.5s noodles up
# endthreading.local(): die eigene Schürzentasche für jeden Koch. Eine Variable mit demselben Namen, die aber in jedem Thread einen anderen Wert enthält. Welche Tasche es ist, hängt davon ab, wer hineingreift.
pocket = threading.local()
def cook(name, n):
pocket.name = name
pocket.count = 0
for _ in range(n):
pocket.count += 1
time.sleep(0.01)
print(pocket.name, pocket.count)
ts = [threading.Thread(target=cook, args=a) for a in [("minho", 3), ("sua", 5)]]
for t in ts:
t.start()
for t in ts:
t.join()
print(hasattr(pocket, "name")) # die Tasche der Chefin (Haupt-Thread) ist leer
# minho 3
# sua 5
# FalseObwohl pocket eine einzige globale Variable ist, vermischen sich Minhos count und Suas count nicht. So gibt man häufig jedem Thread seine eigene Datenbankverbindung.
Tipp der Chefin
Wenn du Threads anlegst, gib ihnen zuerst einen Namen. Wenn du um vier Uhr früh die Logs öffnest und da steht nur Thread-7, Thread-12, Thread-7 …, findest du nie raus, wer's verbockt hat. Pack einfach %(threadName)s ins logging-Format, dann steht in jeder Zeile der Name.
Das Ziel für heute Nacht war, sechs Bestellungen auf drei Leute zu verteilen, in unter 0,6 Sekunden fertig zu sein und festzuhalten, wer nach wie vielen Sekunden was fertig hatte. Wir geben allen ein Namensschild (name=) und lassen jeden nach jedem fertigen Gericht eine Zeile in ein gemeinsames Logbuch schreiben.
start = time.perf_counter()
log = [] # (Fertigzeit, Koch, Bestellung)
def cook(dishes):
me = threading.current_thread().name
for dish in dishes:
time.sleep(0.3)
log.append((round(time.perf_counter() - start, 1), me, dish))
plan = {"minho": ["#1", "#4"], "sua": ["#2", "#5"], "tak": ["#3", "#6"]}
threads = [threading.Thread(target=cook, args=(d,), name=n) for n, d in plan.items()]
for th in threads:
th.start()
for th in threads:
th.join()
for row in sorted(log):
print(*row)
print(f"total {time.perf_counter() - start:.2f}s, {len(log)} orders")
# 0.3 minho #1
# 0.3 sua #2
# 0.3 tak #3
# 0.6 minho #4
# 0.6 sua #5
# 0.6 tak #6
# total 0.60s, 6 ordersargs=(d,) ein Komma.log.append(...) ist eine Listenoperation, die in einem Schritt erledigt ist; auch wenn mehrere Threads sie gleichzeitig aufrufen, geht keine Zeile verloren. Gelesen wird das Logbuch nach join(), sortiert.Die Chefin überfliegt das Logbuch und nickt. „Gut, jetzt wissen wir, wer was gemacht hat. Dann probieren wir morgen noch eine Sache: Wenn vier Leute jeweils ihre Stückzahl auf eine einzige Zahl an der Tafel addieren, stimmt diese Zahl dann?“
Schichtbuch — Zweite Nacht. Erster Arbeitstag für Minho, Sua und Herrn Tak. Mit Namensschildern 6 Bestellungen zu je zweien verteilt, in 0,6 Sekunden fertig. Im Logbuch steht, wer nach wie vielen Sekunden was rausgegeben hat.
start() und join() in derselben Schleife stehen. Schätze vorher, wie viele Sekunden es dauert, und miss dann nach.sleep-Zeit je nach Name des Kochs unterschiedlich setzen). Wer bestimmt jetzt die Gesamtzeit? Vergleiche das mit der Zeitleiste, in der nur eine einzige Reihe länger wird.threading.Timer einen Küchentimer, der „dreimal, alle 0,3 Sekunden“ klingelt. Ein Timer klingelt nur einmal, also musst du beim Klingeln den nächsten Timer neu starten.Rezeptkarte der Chefin
th = threading.Thread(target=funktion, args=(arg,), name="minho") → th.start() → th.join()
run() direkt aufgerufen läuft ohne neuen Thread der Reihe nach im aktuellen Thread. Threads startet man mit start(), und zwar nur einmal.
Wer ist dran: current_thread().name, main_thread(), enumerate(), get_native_id()
Threads liefern keinen Rückgabewert. In vorher angelegte Listenfächer oder dict-Schlüssel legen und nach join() lesen.
Auf daemon=True wird am Programmende nicht gewartet. Nicht für Arbeit verwenden, die fertig werden muss.
Von Thread erben (run überschreiben), Timer (einmal nach n Sekunden, cancel), local() (eigene Tasche für jeden Thread)
← Zurück
Lektion 1 · Die Schlange am ersten Abend: Nebenläufigkeit und Parallelität
Weiter →
Lektion 3 · Die Zahl auf der Tafel stimmt nicht: Race Conditions und Lock
Ü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.