Aufwärmen · Aufgabe 1 von 7
// A3.1 · ca. 30 Min. · Vertiefung
Threads, Locks und der GIL
Nach dieser Lektion lassen Sie Funktionen in Threads laufen und warten auf sie, schützen geteilte Daten mit einem Lock und wissen, wann Threads ein Programm schneller machen.
Lektion 1 von 6 in A3 Nebenläufigkeit
Danach können Sie
- Threads mit Thread(target=..., args=...) starten, joinen und ihre Ergebnisse einsammeln
- Eine Race Condition auf geteilten Daten erkennen und mit einem Lock vom Lesen bis zum Schreiben beheben
- Erklären, was der GIL für CPU- und I/O-lastige Threads bedeutet, und ihn mit sys._is_gil_enabled() prüfen
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es vorher, bevor Sie weiterlesen. Zwei Threads zahlen je 10 ein. Die Barrier sorgt dafür, dass beide den Kontostand lesen, bevor einer schreibt. Was gibt das aus?
import threading balance = 0 both_have_read = threading.Barrier(2) def deposit() -> None: global balance seen = balance # read both_have_read.wait() # wait until the other thread has read too balance = seen + 10 # write threads = [threading.Thread(target=deposit) for _ in range(2)] for t in threads: t.start() for t in threads: t.join() print(balance)Üben · Aufgabe 3 von 7
Setzen Sie die Methode ein, die wartet, bis der Thread fertig ist, damit results beim Ausgeben vollständig ist.
import threading results = [] def work(): results.append("done") t = threading.Thread(target=work) t.start() t.____()t.()Üben · Aufgabe 4 von 7
Vier Threads addieren je tausendmal 1 und halten dabei für jede Addition einen Lock. Was gibt das aus?
import threading counter = 0 lock = threading.Lock() def add_many(): global counter for _ in range(1000): with lock: counter += 1 threads = [threading.Thread(target=add_many) for _ in range(4)] for t in threads: t.start() for t in threads: t.join() print(counter)Üben · Aufgabe 5 von 7
Ordnen Sie jedem Aufruf zu, was er tut.
Denksport · Aufgabe 6 von 7
Knobelaufgabe. Der Thread heißt worker. Was gibt das aus?
import threading def work(): print("working in", threading.current_thread().name) t = threading.Thread(target=work(), name="worker") t.start() t.join()Anwenden · Aufgabe 7 von 7
Mini-Aufgabe, auf Ihrem eigenen Rechner. Schreiben Sie ein Programm, das für jedes Wort in ["alpha", "be", "gamma"] einen Thread startet. Jeder Thread speichert len(word) unter seinem Wort in einem Dict und addiert es unter einem Lock zu einer geteilten Summe. Geben Sie nach dem Joinen aller Threads die sortierten Dict-Einträge und die Summe aus. Geben Sie dann sys._is_gil_enabled() aus, um zu sehen, ob der GIL in Ihrem Prozess aktiv ist.
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
Wörter in drei Dokumenten gleichzeitig zählen
Drei Threads zählen je die Wörter eines Dokuments. Jeder Thread schreibt seinen eigenen Schlüssel in word_counts, diese Schreibzugriffe kommen sich also nie in die Quere. Alle drei addieren aber auf die geteilte Summe total, deshalb schützt ein Lock diese Addition. Der Haupt-Thread startet alle drei, joint sie und liest erst dann die Ergebnisse. Speichern Sie es als main.py und starten Sie python main.py (unter macOS und Linux python3 main.py) auf Ihrem Rechner: Im Browser lassen sich keine Threads starten.
main.py
import threading
DOCUMENTS = {
"intro.txt": "threads share memory and take turns under the GIL",
"locks.txt": "a lock lets one thread at a time change shared data",
"join.txt": "join waits until a thread has finished",
}
word_counts: dict[str, int] = {}
total = 0
total_lock = threading.Lock()
def count_words(name: str, text: str) -> None:
global total
n = len(text.split())
word_counts[name] = n # each thread writes its own key
with total_lock: # read, add, write back: one thread at a time
total += n
threads = [threading.Thread(target=count_words, args=(name, text)) for name, text in DOCUMENTS.items()]
for t in threads:
t.start()
for t in threads:
t.join() # wait for every thread before reading the results
for name in DOCUMENTS:
print(f"{name}: {word_counts[name]} words")
print("total:", total)
print("still running:", sum(t.is_alive() for t in threads))
Ausführen mit
python main.pyAusgabe
intro.txt: 9 words
locks.txt: 11 words
join.txt: 7 words
total: 27
still running: 0- Die Ergebnisse erscheinen in der Reihenfolge von DOCUMENTS, nicht in der, in der die Threads zufällig fertig wurden.
- args=(name, text) ist ein Tupel; Thread ruft count_words(name, text) im neuen Thread auf.
- total += n ist durch total_lock geschützt, also ist die Summe bei jedem Lauf 27.
- still running: 0 zeigt, dass join() auf alle drei Threads gewartet hat.
Übungen
Übung 1 von 2
Jobs in Threads ausführen
Vervollständigen Sie run_all(jobs) in main.py. jobs ist eine Liste von Funktionen ohne Argumente, die einen String liefern. Führen Sie jeden Job in einem eigenen Thread aus, starten Sie alle, bevor Sie einen joinen, und liefern Sie die Ergebnisse in der Reihenfolge von jobs. Ein Thread kann nichts zurückgeben, also bekommt jeder einen eigenen Platz in einer Liste. Arbeiten Sie auf Ihrem Rechner, denn im Browser starten keine Threads: python main.py zeigt eine kurze Demo, python learnrun.py test führt die Tests aus.
Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
Hinweise
Hinweis 1
Legen Sie zuerst results = [""] * len(jobs) an und eine kleine innere Funktion run(i, job), die job() in results[i] speichert.
Hinweis 2
Erzeugen Sie pro Job einen Thread mit target=run und args=(i, job); enumerate(jobs) liefert Ihnen i.
Hinweis 3
Zwei Schleifen: eine, die auf jedem Thread start() aufruft, danach eine, die auf jedem join() aufruft.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import threading
from collections.abc import Callable
def run_all(jobs: list[Callable[[], str]]) -> list[str]:
"""Run each job in its own thread; return the results in the order of jobs."""
results = [""] * len(jobs)
def run(i: int, job: Callable[[], str]) -> None:
results[i] = job()
threads = [threading.Thread(target=run, args=(i, job)) for i, job in enumerate(jobs)]
for t in threads:
t.start()
for t in threads:
t.join()
return results
if __name__ == "__main__":
print(run_all([lambda: "one", lambda: "two", lambda: threading.current_thread().name]))
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import threading
from collections.abc import Callable
def run_all(jobs: list[Callable[[], str]]) -> list[str]:
"""Run each job in its own thread; return the results in the order of jobs."""
# This runs the jobs one after another in the main thread.
# Start one threading.Thread per job, then join them all.
return [job() for job in jobs]
if __name__ == "__main__":
print(run_all([lambda: "one", lambda: "two", lambda: threading.current_thread().name]))
test_main.py
import threading
from main import run_all
def test_order():
"""Die Ergebnisse kommen in der Reihenfolge der Jobs zurück"""
got = run_all([lambda: "a", lambda: "b", lambda: "c"])
assert got == ["a", "b", "c"], f"run_all lieferte {got!r}, erwartet war ['a', 'b', 'c']"
def test_not_in_main_thread():
"""Jeder Job läuft außerhalb des Haupt-Threads"""
got = run_all([lambda: threading.current_thread().name for _ in range(3)])
assert "MainThread" not in got, f"die Jobs liefen in {got!r}: starten Sie für jeden Job einen Thread"
def test_all_at_once():
"""Alle drei Jobs laufen gleichzeitig und treffen sich an einer Barrier"""
barrier = threading.Barrier(3, timeout=0.5)
def job():
barrier.wait()
return "met"
try:
got = run_all([job, job, job])
except threading.BrokenBarrierError:
got = []
assert got == ["met", "met", "met"], f"run_all lieferte {got!r}: starten Sie alle Threads, bevor Sie einen joinen"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 2
Ein Konto ohne verlorene Einzahlungen
Account.deposit liest den Kontostand, ruft pause() auf und schreibt den neuen Stand. Die Tests übergeben ein pause(), das zwei Threads zwischen Lesen und Schreiben festhält, sodass eine Einzahlung verloren geht. Reparieren Sie deposit mit dem Lock des Kontos, sodass zwei gleichzeitige Einzahlungen von 10 immer 20 ergeben. Lesen, pause() und Schreiben gehören in den Lock. python main.py führt zwei Einzahlungen in Threads aus; das ungünstige Timing erzwingen nur die Tests.
Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
Hinweise
Hinweis 1
Der Fehler liegt zwischen seen = self.balance und dem Schreiben: Dort kann ein anderer Thread denselben alten Stand lesen.
Hinweis 2
with self._lock: erwirbt den Lock und gibt ihn am Ende des Blocks frei, auch bei einer Exception.
Hinweis 3
Rücken Sie alle drei Zeilen, das Lesen, pause() und das Schreiben, unter with self._lock: ein.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import threading
from collections.abc import Callable
class Account:
def __init__(self) -> None:
self.balance = 0
self._lock = threading.Lock()
def deposit(self, amount: int, pause: Callable[[], None] = lambda: None) -> None:
"""Add amount to the balance. pause() runs between the read and the write."""
with self._lock:
seen = self.balance
pause()
self.balance = seen + amount
if __name__ == "__main__":
account = Account()
threads = [threading.Thread(target=account.deposit, args=(10,)) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
print("balance:", account.balance)
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import threading
from collections.abc import Callable
class Account:
def __init__(self) -> None:
self.balance = 0
self._lock = threading.Lock()
def deposit(self, amount: int, pause: Callable[[], None] = lambda: None) -> None:
"""Add amount to the balance. pause() runs between the read and the write."""
# Two threads can both read the old balance here. Use self._lock.
seen = self.balance
pause()
self.balance = seen + amount
if __name__ == "__main__":
account = Account()
threads = [threading.Thread(target=account.deposit, args=(10,)) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
print("balance:", account.balance)
test_main.py
import threading
from main import Account
def deposit_together(account, amount, pause):
threads = [threading.Thread(target=account.deposit, args=(amount, pause)) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
return account.balance
def test_one_deposit():
"""Eine Einzahlung von 25 ergibt einen Stand von 25"""
account = Account()
account.deposit(25)
assert account.balance == 25, f"der Stand ist {account.balance}, erwartet war 25"
def test_no_lost_update():
"""Zwei gleichzeitige Einzahlungen von 10 ergeben 20"""
barrier = threading.Barrier(2, timeout=0.05)
def pause():
try:
barrier.wait() # both threads stop here, between read and write
except threading.BrokenBarrierError:
pass
got = deposit_together(Account(), 10, pause)
assert got == 20, f"der Stand ist {got}, erwartet war 20: eine Einzahlung ging verloren, halten Sie den Lock vom Lesen bis zum Schreiben"
def test_lock_is_free_afterwards():
"""Der Lock ist nach jeder Einzahlung wieder frei"""
account = Account()
account.deposit(5)
account.deposit(5)
assert not account._lock.locked(), "der Lock ist nach deposit() noch belegt: nutzen Sie with self._lock:"
assert account.balance == 10, f"der Stand ist {account.balance}, erwartet war 10"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHäufige Fehler
Denselben Thread zweimal starten
import threading
def work():
pass
t = threading.Thread(target=work)
t.start()
t.join()
t.start()
Was Python ausgibt
RuntimeError: threads can only be started onceWarum, und die Lösung
Ein Thread-Objekt führt sein target einmal aus. Für einen weiteren Lauf erzeugen Sie einen neuen Thread: threading.Thread(target=work).start(). Für viele wiederkehrende Jobs verwendet ein Thread-Pool (concurrent.futures, zwei Lektionen weiter) seine Threads für Sie wieder.
Einen Thread joinen, bevor er gestartet ist
import threading
def work():
pass
t = threading.Thread(target=work)
t.join()
t.start()
Was Python ausgibt
RuntimeError: cannot join thread before it is startedWarum, und die Lösung
join() wartet auf das Ende eines laufenden Threads, der Thread muss also gestartet sein. Rufen Sie erst auf jedem Thread start() auf, dann auf jedem join().
Einen Lock freigeben, den man nicht hält
import threading
lock = threading.Lock()
lock.release()
Was Python ausgibt
RuntimeError: release unlocked lockWarum, und die Lösung
release() ist nur auf einem gesperrten Lock erlaubt. acquire() und release() von Hand zu paaren geht leicht schief, vor allem wenn eine Exception das release überspringt. Schreiben Sie stattdessen with lock:. Das erwirbt beim Eintritt und gibt beim Verlassen immer frei.
Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode
Abschlussquiz
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.