HOWTO · Python

Thread-Sperre in Python

Eine Sperre koordiniert kooperierende Threads beim Zugriff auf gemeinsam genutzten Zustand. In Python lässt sie sich mit einer Instanz der Lock-Klasse verwenden.

Auf dieser Seite

Verwenden Sie eine Sperre, wenn mehrere Python-Threads auf gemeinsam genutzten Zustand zugreifen und mindestens einer davon ihn verändert. Eine Sperre koordiniert zusammenarbeitende Threads in einem kritischen Abschnitt, also in dem Code, der die gemeinsam genutzten Daten liest oder verändert. Diese Anleitung zeigt, wie Sie eine Race Condition erkennen, einen gemeinsam genutzten Zähler mit threading.Lock schützen und die empfohlene Form with lock: verwenden.

Eine Race Condition in gemeinsam genutztem Zustand erkennen

Eine Race Condition tritt auf, wenn das Ergebnis eines Programms vom zeitlichen Ablauf oder von der Reihenfolge nebenläufiger Vorgänge abhängt. Einen Zähler zu aktualisieren ist ein Lese-Berechne-Schreib-Vorgang: Ein Thread liest den aktuellen Wert, berechnet einen neuen Wert und speichert ihn anschließend. Lesen zwei Threads denselben alten Wert, bevor einer von ihnen seine Änderung speichert, kann eine Aktualisierung die andere überschreiben.

Das folgende Programm startet zwei Threads, die denselben Zähler jeweils erhöhen. Die Ausführungsreihenfolge der Threads ist nicht festgelegt. Daher können die ausgegebenen Zwischenwerte und der Endwert variieren. Insbesondere ist ein Endergebnis von 30 nicht garantiert.

Führen Sie dieses Beispiel aus:

Der Code und die angezeigten Bezeichnungen oder Ausgaben in diesem Abschnitt bleiben auf Englisch.

from threading import Thread

counter = 0


def increase(amount):
    global counter
    current = counter
    current += amount
    counter = current
    print(f"counter={counter}")


first = Thread(target=increase, args=(10,))
second = Thread(target=increase, args=(20,))
first.start()
second.start()
first.join()
second.join()
print(f"The final counter is {counter}")

Bei einem einzelnen Lauf kann zufällig der Endwert 30 ausgegeben werden. Dadurch wird die Aktualisierung jedoch nicht sicher. Entscheidend ist, dass mehrere Threads denselben Zustand ohne Abstimmung lesen und verändern. Verwenden Sie eine bestimmte Ausgabereihenfolge nicht als Beweis dafür, dass der Code korrekt ist.

Gemeinsam genutzten Zustand mit threading.Lock schützen

threading.Lock ist ein Mechanismus zur gegenseitigen Ausschließung. Erstellen Sie eine Sperre und teilen Sie sie mit den Threads, die sich abstimmen müssen. Erwerben Sie die Sperre, bevor Sie den kritischen Abschnitt betreten, und geben Sie sie frei, nachdem alle zugehörigen Lese- und Schreibzugriffe abgeschlossen sind. Der kritische Abschnitt muss die gesamte Lese-Berechne-Schreib-Folge umfassen; wird nur die abschließende Zuweisung geschützt, kann weiterhin eine Aktualisierung verloren gehen. Weitere Informationen zur API von Lock finden Sie in der Python-Dokumentation zu threading.

Führen Sie dieses Beispiel aus:

Der Code und die angezeigten Bezeichnungen oder Ausgaben in diesem Abschnitt bleiben auf Englisch.

from threading import Lock, Thread

counter = 0
lock = Lock()


def increase(amount):
    global counter
    lock.acquire()
    current = counter
    current += amount
    counter = current
    lock.release()


threads = [Thread(target=increase, args=(amount,)) for amount in (10, 20)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()
print(counter)

Auf jeden erfolgreichen Aufruf von acquire() muss ein passender Aufruf von release() folgen. Wird ein nicht gesperrtes Lock freigegeben, wird RuntimeError ausgelöst. Verlässt der Code den kritischen Abschnitt, bevor release() erreicht wird, etwa weil eine Ausnahme auftritt, kann die Sperre gehalten bleiben und andere Threads können unbegrenzt warten. Die Verwendung eines Kontextmanagers vermeidet dieses Aufräumproblem im normalen Einsatz.

Eine Sperre mit einem Kontextmanager verwenden

Verwenden Sie with lock:, damit die Sperre beim Eintritt in den Block erworben und beim Verlassen des Blocks freigegeben wird. Die Freigabe erfolgt sowohl nach regulärem Abschluss des Blocks als auch dann, wenn eine Ausnahme zum Verlassen führt. Alle Vorgänge, die abgestimmt werden müssen, gehören in den Block; Vorgänge außerhalb davon sind nicht geschützt.

Führen Sie dieses Beispiel aus:

Der Code und die angezeigten Bezeichnungen oder Ausgaben in diesem Abschnitt bleiben auf Englisch.

from threading import Lock, Thread

counter = 0
lock = Lock()


def increase(amount):
    global counter
    with lock:
        current = counter
        current += amount
        counter = current


threads = [Thread(target=increase, args=(amount,)) for amount in (10, 20)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()
print(counter)

Die Sperre verhindert, dass sich die beiden Erhöhungen innerhalb des kritischen Abschnitts überschneiden, sodass beide Aktualisierungen erhalten bleiben. Der Endwert des Zählers ist daher 30, auch wenn weder die Ausführungsreihenfolge der Threads noch die Zwischenwerte garantiert sind. Bei einer Sperre zum Schutz gemeinsam genutzter Daten lässt sich diese Form leichter überprüfen, weil der Block Erwerb und Freigabe strukturell zusammenfasst.

Sperren sicher anwenden

Alle Codepfade, die einen abgestimmten Zugriff auf denselben Zustand benötigen, müssen dieselbe Sperre verwenden. Eine Sperre schützt keinen Zugriff, der sie umgeht, und macht unabhängige Vorgänge nicht atomar. Schützen verschiedene Sperren dieselben Daten, stimmen sie sich nicht miteinander ab. Legen Sie fest, welche gemeinsam genutzten Daten zu einem kritischen Abschnitt gehören, und verwenden Sie für jeden zu synchronisierenden Lese- oder Schreibzugriff durchgängig dieselbe Sperre.

Halten Sie den kritischen Abschnitt so klein, wie es die Korrektheit zulässt. So können unabhängige Vorgänge weiterlaufen, während die Sperre frei ist, und andere Threads müssen kürzer warten. Vermeiden Sie Aufrufe von Code, der lange blockieren kann, während Sie eine Sperre halten. Vermeiden Sie außerdem, mehrere Sperren in verschiedenen Threads in uneinheitlicher Reihenfolge zu erwerben, da dies zu einem Deadlock führen kann.

Bevorzugen Sie im normalen Anwendungscode with lock:, damit die Sperre beim Verlassen des Blocks automatisch freigegeben wird. Verwenden Sie acquire() und release() nur dann ausdrücklich, wenn der Kontrollfluss eine manuelle Verwaltung erfordert, und stellen Sie sicher, dass die Freigabe auch bei vorzeitigem Verlassen erfolgt. Eine Sperre ist ein Werkzeug zur Abstimmung, keine allgemeine Lösung für jedes Nebenläufigkeitsproblem: Sie schützt nur den Zustand und die Vorgänge, die konsequent am selben Sperrkonzept teilnehmen.