Zum Inhalt springen
aviral gupta

// 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

Anfang des Moduls

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
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Modul A1: Ein with-Block nutzt das Context-Manager-Protokoll. Welche Aussagen stimmen? Wählen Sie alle zutreffenden.

    Wählen Sie alle zutreffenden aus.

  2. 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)
  3. Ü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.()
  4. Ü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)
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jedem Aufruf zu, was er tut.

  6. 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()
  7. 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.py

Ausgabe

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
  1. Hinweis 1

    Legen Sie zuerst results = [""] * len(jobs) an und eine kleine innere Funktion run(i, job), die job() in results[i] speichert.

  2. Hinweis 2

    Erzeugen Sie pro Job einen Thread mit target=run und args=(i, job); enumerate(jobs) liefert Ihnen i.

  3. 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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.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
  1. Hinweis 1

    Der Fehler liegt zwischen seen = self.balance und dem Schreiben: Dort kann ein anderer Thread denselben alten Stand lesen.

  2. Hinweis 2

    with self._lock: erwirbt den Lock und gibt ihn am Ende des Blocks frei, auch bei einer Exception.

  3. 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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Hä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 once

Warum, 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 started

Warum, 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 lock

Warum, 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.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Erst start, dann join

threading.Thread(target=f, args=(x,)) erzeugt ein Thread-Objekt; noch läuft nichts. start() führt f(x) in einem neuen Thread aus und darf pro Thread-Objekt nur einmal aufgerufen werden. join() blockiert den aufrufenden Thread, bis dieser Thread fertig ist. Starten Sie erst alle Threads und joinen Sie danach, sonst laufen sie nacheinander. Übergeben Sie die Funktion selbst als target, nicht f(): f() läuft sofort im aktuellen Thread. Einen Rückgabewert liefert ein Thread nicht; er schreibt sein Ergebnis in einen eigenen Listenplatz oder Dict-Schlüssel.

Race Conditions und Locks

balance += 10 sind drei Schritte: lesen, addieren, schreiben. Lesen zwei Threads, bevor einer schreibt, geht ein Update verloren. Das ist eine Race Condition: Das Ergebnis hängt vom zeitlichen Ablauf der Threads ab. Ein threading.Lock lässt immer nur einen Thread durch: with lock: erwirbt ihn beim Eintritt und gibt ihn beim Verlassen frei, auch bei einer Exception. Halten Sie den Lock für das ganze Lesen-Ändern-Schreiben, nicht für jeden Schritt einzeln. Die Free-Threading-Doku empfiehlt einen Lock statt der internen Locks eingebauter Typen.

Der GIL und der Free-threaded-Build

Im Standard-Build von CPython lässt der Global Interpreter Lock (GIL) immer nur einen Thread Python-Bytecode ausführen. Für I/O-lastige Arbeit helfen Threads trotzdem: Der GIL wird freigegeben, während ein Thread auf eine Datei oder das Netzwerk wartet. CPU-lastiger Python-Code wird mit Threads nicht schneller; dafür gibt es Prozesse (nächste Lektion). Seit 3.13 gibt es einen optionalen Free-threaded-Build, in dem der GIL abgeschaltet ist und Threads parallel laufen; in 3.14 ist er unterstützt und nicht mehr experimentell, aber weiter optional. sys._is_gil_enabled() liefert True, wenn der GIL im laufenden Prozess aktiv ist.

Quellen

Zuletzt geprüft am 29. September 2026