Garantien zur Threadsicherheit¶
Diese Seite dokumentiert die Garantien zur Threadsicherheit für eingebaute Typen im Build mit freien Threads von Python. Die hier beschriebenen Garantien gelten, wenn Python mit abgeschaltetem GIL läuft (Modus mit freien Threads). Ist der GIL eingeschaltet, werden die meisten Operationen implizit serialisiert.
Allgemeine Hinweise zum Schreiben threadsicheren Codes in Python mit freien Threads findest du unter Unterstützung von freiem Threading in Python.
Stufen der Threadsicherheit¶
Die Dokumentation des C-API verwendet die folgenden Stufen, um die Garantien zur Threadsicherheit jeder Funktion zu beschreiben. Die Stufen sind von der unsichersten zur sichersten aufgeführt.
Unverträglich¶
Eine Funktion oder Operation, die sich auch mit äußerer Synchronisierung nicht für die gleichzeitige Verwendung sicher machen lässt. Unverträglicher Code greift typischerweise unsynchronisiert auf globalen Zustand zu und darf während der gesamten Laufzeit des Programms nur aus einem einzigen Thread aufgerufen werden.
Beispiel: eine Funktion, die prozessweiten Zustand wie Signal-Handler oder Umgebungsvariablen verändert, bei der gleichzeitige Aufrufe aus beliebigen Threads selbst mit äußerer Sperre mit der Laufzeitumgebung oder anderen Bibliotheken in Konflikt geraten können.
Verträglich¶
Eine Funktion oder Operation, die sich sicher aus mehreren Threads aufrufen lässt, vorausgesetzt, der Aufrufer sorgt für geeignete äußere Synchronisierung, etwa indem er für die Dauer jedes Aufrufs eine Sperre hält. Ohne solche Synchronisierung können gleichzeitige Aufrufe zu Race-Conditions oder Datenkonflikten führen.
Beispiel: eine Funktion, die aus einem Objekt liest oder in es schreibt, dessen interner Zustand nicht durch eine Sperre geschützt ist. Aufrufer müssen sicherstellen, dass niemals zwei Threads gleichzeitig auf dasselbe Objekt zugreifen.
Sicher auf verschiedenen Objekten¶
Eine Funktion oder Operation, die sich ohne äußere Synchronisierung sicher aus mehreren Threads aufrufen lässt, solange jeder Thread auf einem anderen Objekt arbeitet. Zwei Threads dürfen die Funktion gleichzeitig aufrufen, sie dürfen ihr aber nicht dasselbe Objekt (oder Objekte, die sich zugrunde liegenden Zustand teilen) als Argument übergeben.
Beispiel: eine Funktion, die Felder eines Structs mit nicht atomaren Schreibzugriffen verändert. Zwei Threads können die Funktion jeweils sicher auf ihrer eigenen Struct-Instanz aufrufen, gleichzeitige Aufrufe auf derselben Instanz brauchen aber äußere Synchronisierung.
Atomar¶
Eine Funktion oder Operation, die anderen Threads gegenüber atomar erscheint – aus Sicht der anderen Threads läuft sie augenblicklich ab. Das ist die stärkste Form der Threadsicherheit.
Beispiel: PyMutex_IsLocked() liest den Zustand des Mutex atomar und lässt sich jederzeit aus jedem Thread aufrufen.
Threadsicherheit bei list-Objekten¶
Ein einzelnes Element aus einer list zu lesen ist atomar:
lst[i] # list.__getitem__
Die folgenden Methoden durchlaufen die Liste und verwenden atomare Lesezugriffe auf jedes Element, um ihre Aufgabe zu erfüllen. Das heißt, dass sie Ergebnisse zurückgeben können, die von gleichzeitigen Änderungen beeinflusst sind:
item in lst
lst.index(item)
lst.count(item)
Alle obigen Operationen vermeiden das Erwerben objektspezifischer Sperren. Sie blockieren gleichzeitige Änderungen nicht. Andere Operationen, die eine Sperre halten, verhindern nicht, dass diese hier Zwischenzustände beobachten.
Alle weiteren Operationen von hier an blockieren über die objektspezifische Sperre.
Ein einzelnes Element über lst[i] = x zu schreiben lässt sich sicher aus mehreren Threads aufrufen und beschädigt die Liste nicht.
Die folgenden Operationen geben neue Objekte zurück und erscheinen anderen Threads atomar:
lst1 + lst2 # verkettet zwei Listen zu einer neuen Liste
x * lst # wiederholt lst x-mal in einer neuen Liste
lst.copy() # gibt eine flache Kopie der Liste zurück
Die folgenden Methoden, die nur auf einem einzelnen Element arbeiten und kein Verschieben erfordern, sind atomar:
lst.append(x) # hängt am Ende der Liste an, kein Verschieben nötig
lst.pop() # entnimmt ein Element am Ende der Liste, kein Verschieben nötig
Die Methode clear() ist ebenfalls atomar. Andere Threads können das Entfernen der Elemente nicht beobachten.
Die Methode sort() ist nicht atomar. Andere Threads können zwar keine Zwischenzustände während des Sortierens beobachten, die Liste erscheint aber für die Dauer des Sortierens leer.
Die folgenden Operationen können sperrfreien Operationen erlauben, Zwischenzustände zu beobachten, denn sie verändern mehrere Elemente an Ort und Stelle:
lst.insert(idx, item) # verschiebt Elemente
lst.pop(idx) # idx nicht am Ende der Liste, verschiebt Elemente
lst *= x # kopiert Elemente an Ort und Stelle
Die Methode remove() kann gleichzeitige Änderungen zulassen, weil der Vergleich der Elemente beliebigen Python-Code ausführen kann (über __eq__()).
extend() lässt sich sicher aus mehreren Threads aufrufen. Die Garantien hängen allerdings vom übergebenen Iterable ab. Ist es eine list, ein tuple, ein set, ein frozenset, ein dict oder ein Dictionary-View-Objekt (nicht aber eine Unterklasse davon), dann ist die extend-Operation vor gleichzeitigen Änderungen am Iterable sicher. Andernfalls wird ein Iterator erzeugt, der von einem anderen Thread gleichzeitig verändert werden kann. Dasselbe gilt für das Verketten einer Liste mit anderen Iterables an Ort und Stelle über lst += iterable.
Ebenso lässt sich die Zuweisung an ein Listen-Slice über lst[i:j] = iterable sicher aus mehreren Threads aufrufen, iterable wird aber nur dann gesperrt, wenn es ebenfalls eine list ist (nicht aber eine Unterklasse davon).
Operationen mit mehreren Zugriffen sowie das Iterieren sind nie atomar. Zum Beispiel:
# NICHT atomar: lesen, ändern, schreiben
lst[i] = lst[i] + 1
# NICHT atomar: prüfen, dann handeln
if lst:
item = lst.pop()
# NICHT threadsicher: iterieren während des Änderns
for item in lst:
process(item) # ein anderer Thread kann lst verändern
Zieh äußere Synchronisierung in Erwägung, wenn du list-Instanzen über Threads hinweg gemeinsam nutzt.
Threadsicherheit bei dict-Objekten¶
Ein Dictionary mit dem Konstruktor dict zu erzeugen ist atomar, wenn das Argument ein dict oder ein tuple ist. Bei der Methode dict.fromkeys() ist das Erzeugen des Dictionarys atomar, wenn das Argument ein dict, tuple, set oder frozenset ist.
Die folgenden Operationen und Funktionen sind sperrfrei und atomar.
d[key] # dict.__getitem__
d.get(key) # dict.get
key in d # dict.__contains__
len(d) # dict.__len__
Alle weiteren Operationen von hier an halten die objektspezifische Sperre.
Ein einzelnes Element zu schreiben oder zu entfernen lässt sich sicher aus mehreren Threads aufrufen und beschädigt das Dictionary nicht:
d[key] = value # schreiben
del d[key] # löschen
d.pop(key) # entfernen und zurückgeben
d.popitem() # letzten Eintrag entfernen und zurückgeben
d.setdefault(key, v) # einfügen, falls nicht vorhanden
Diese Operationen können Schlüssel über __eq__() vergleichen, was beliebigen Python-Code ausführen kann. Während solcher Vergleiche kann das Dictionary von einem anderen Thread verändert werden. Bei eingebauten Typen wie str, int und float, die __eq__() in C umsetzen, wird die zugrunde liegende Sperre während der Vergleiche nicht freigegeben. Dort stellt sich das Problem nicht.
Die folgenden Operationen geben neue Objekte zurück und halten die objektspezifische Sperre für die Dauer der Operation:
d.copy() # gibt eine flache Kopie des Dictionarys zurück
d | other # führt zwei Dictionarys zu einem neuen zusammen
d.keys() # gibt ein neues dict_keys-View-Objekt zurück
d.values() # gibt ein neues dict_values-View-Objekt zurück
d.items() # gibt ein neues dict_items-View-Objekt zurück
Die Methode clear() hält die Sperre für ihre Dauer. Andere Threads können das Entfernen der Elemente nicht beobachten.
Die folgenden Operationen sperren beide Dictionarys. Für update() und |= gilt das nur, wenn der andere Operand ein dict ist, das den üblichen dict-Iterator verwendet (nicht aber eine Unterklasse, die das Iterieren überschreibt). Für den Gleichheitsvergleich gilt es für dict und seine Unterklassen:
d.update(other_dict) # beide gesperrt, wenn other_dict ein dict ist
d |= other_dict # beide gesperrt, wenn other_dict ein dict ist
d == other_dict # beide gesperrt bei dict und Unterklassen
Alle Vergleichsoperationen vergleichen auch die Werte über __eq__(). Bei nicht eingebauten Typen kann die Sperre während des Vergleichs daher freigegeben werden.
fromkeys() sperrt sowohl das neue Dictionary als auch das Iterable, wenn das Iterable genau ein dict, set oder frozenset ist (keine Unterklasse):
dict.fromkeys(a_dict) # sperrt beide
dict.fromkeys(a_set) # sperrt beide
dict.fromkeys(a_frozenset) # sperrt beide
Wird aus einem Iterable aktualisiert, das kein dict ist, wird nur das Ziel-Dictionary gesperrt. Das Iterable kann von einem anderen Thread gleichzeitig verändert werden:
d.update(iterable) # iterable ist kein dict: nur d gesperrt
d |= iterable # iterable ist kein dict: nur d gesperrt
dict.fromkeys(iterable) # iterable ist kein dict/set/frozenset: nur das Ergebnis gesperrt
Operationen mit mehreren Zugriffen sowie das Iterieren sind nie atomar:
# NICHT atomar: lesen, ändern, schreiben
d[key] = d[key] + 1
# NICHT atomar: prüfen, dann handeln (TOCTOU)
if key in d:
del d[key]
# NICHT threadsicher: iterieren während des Änderns
for key, value in d.items():
process(key) # ein anderer Thread kann d verändern
Um Probleme zwischen Prüfzeitpunkt und Nutzungszeitpunkt (time-of-check to time-of-use, TOCTOU) zu vermeiden, verwende atomare Operationen oder behandle Ausnahmen:
# pop() mit Standardwert verwenden statt prüfen, dann löschen
d.pop(key, None)
# Oder die Ausnahme behandeln
try:
del d[key]
except KeyError:
pass
Um sicher über ein Dictionary zu iterieren, das von einem anderen Thread verändert werden kann, iteriere über eine Kopie:
# Eine Kopie anlegen, um sicher zu iterieren
for key, value in d.copy().items():
process(key)
Zieh äußere Synchronisierung in Erwägung, wenn du dict-Instanzen über Threads hinweg gemeinsam nutzt.
Threadsicherheit bei set-Objekten¶
Die Funktion len() ist sperrfrei und atomar.
Die folgende Leseoperation ist sperrfrei. Sie blockiert gleichzeitige Änderungen nicht und kann Zwischenzustände von Operationen beobachten, die die objektspezifische Sperre halten:
elem in s # set.__contains__
Diese Operation kann Elemente über __eq__() vergleichen, was beliebigen Python-Code ausführen kann. Während solcher Vergleiche kann die Menge von einem anderen Thread verändert werden. Bei eingebauten Typen wie str, int und float gibt __eq__() die zugrunde liegende Sperre während der Vergleiche nicht frei. Dort stellt sich das Problem nicht.
Alle weiteren Operationen von hier an halten die objektspezifische Sperre.
Ein einzelnes Element hinzuzufügen oder zu entfernen lässt sich sicher aus mehreren Threads aufrufen und beschädigt die Menge nicht:
s.add(elem) # Element hinzufügen
s.remove(elem) # Element entfernen, Ausnahme auslösen, falls nicht vorhanden
s.discard(elem) # Element entfernen, falls vorhanden
s.pop() # ein beliebiges Element entfernen und zurückgeben
Diese Operationen vergleichen ebenfalls Elemente, es gelten also dieselben Überlegungen zu __eq__() wie oben.
Die Methode copy() gibt ein neues Objekt zurück und hält die objektspezifische Sperre für ihre Dauer, sodass sie immer atomar ist.
Die Methode clear() hält die Sperre für ihre Dauer. Andere Threads können das Entfernen der Elemente nicht beobachten.
Die folgenden Operationen nehmen nur set oder frozenset als Operanden und sperren immer beide Objekte:
s |= other # other muss ein set/frozenset sein
s &= other # other muss ein set/frozenset sein
s -= other # other muss ein set/frozenset sein
s ^= other # other muss ein set/frozenset sein
s & other # other muss ein set/frozenset sein
s | other # other muss ein set/frozenset sein
s - other # other muss ein set/frozenset sein
s ^ other # other muss ein set/frozenset sein
set.update(), set.union(), set.intersection() und set.difference() können mehrere Iterables als Argumente nehmen. Sie durchlaufen alle übergebenen Iterables und tun Folgendes:
set.update()andset.union()lock both objects only when the other operand is aset,frozenset, ordict.
set.intersection()andset.difference()always try to lock all objects.
set.symmetric_difference() versucht, beide Objekte zu sperren.
Die aktualisierenden Varianten der obigen Methoden unterscheiden sich untereinander ebenfalls in einigen Punkten:
set.difference_update()andset.intersection_update()try to lock all objects one-by-one.
set.symmetric_difference_update()only locks the arguments if it is of typeset,frozenset, ordict.
Die folgenden Methoden versuchen immer, beide Objekte zu sperren:
s.isdisjoint(other) # beide gesperrt
s.issubset(other) # beide gesperrt
s.issuperset(other) # beide gesperrt
Operationen mit mehreren Zugriffen sowie das Iterieren sind nie atomar:
# NICHT atomar: prüfen, dann handeln
if elem in s:
s.remove(elem)
# NICHT threadsicher: iterieren während des Änderns
for elem in s:
process(elem) # ein anderer Thread kann s verändern
Zieh äußere Synchronisierung in Erwägung, wenn du set-Instanzen über Threads hinweg gemeinsam nutzt. Mehr dazu unter Unterstützung von freiem Threading in Python.
Threadsicherheit bei bytearray-Objekten¶
Die Funktion
len()ist sperrfrei und atomar.Verkettung und Vergleiche verwenden das Puffer-Protokoll, das eine Größenänderung verhindert, aber die objektspezifische Sperre nicht hält. Diese Operationen können Zwischenzustände gleichzeitiger Änderungen beobachten:
ba + other # kann gleichzeitige Schreibzugriffe beobachten ba == other # kann gleichzeitige Schreibzugriffe beobachten ba < other # kann gleichzeitige Schreibzugriffe beobachtenAlle weiteren Operationen von hier an halten die objektspezifische Sperre.
Ein einzelnes Element oder ein Slice zu lesen lässt sich sicher aus mehreren Threads aufrufen:
ba[i] # bytearray.__getitem__ ba[i:j] # sliceDie folgenden Operationen lassen sich sicher aus mehreren Threads aufrufen und beschädigen das bytearray nicht:
ba[i] = x # einzelnes Byte schreiben ba[i:j] = values # Slice schreiben ba.append(x) # einzelnes Byte anhängen ba.extend(other) # mit einem Iterable erweitern ba.insert(i, x) # einzelnes Byte einfügen ba.pop() # letztes Byte entfernen und zurückgeben ba.pop(i) # Byte am Index entfernen und zurückgeben ba.remove(x) # erstes Vorkommen entfernen ba.reverse() # an Ort und Stelle umkehren ba.clear() # alle Bytes entfernenDie Zuweisung an ein Slice sperrt beide Objekte, wenn values ein
bytearrayist:ba[i:j] = other_bytearray # beide gesperrtDie folgenden Operationen geben neue Objekte zurück und halten die objektspezifische Sperre für ihre Dauer:
ba.copy() # gibt eine flache Kopie zurück ba * n # in ein neues bytearray wiederholenDer Test auf Enthaltensein hält die Sperre für seine Dauer:
x in ba # bytearray.__contains__Alle anderen Methoden von bytearray (etwa
find(),replace(),split(),decode()und so weiter) halten die objektspezifische Sperre für ihre Dauer.Operationen mit mehreren Zugriffen sowie das Iterieren sind nie atomar:
# NICHT atomar: prüfen, dann handeln if x in ba: ba.remove(x) # NICHT threadsicher: iterieren während des Änderns for byte in ba: process(byte) # ein anderer Thread kann ba verändernUm sicher über ein bytearray zu iterieren, das von einem anderen Thread verändert werden kann, iteriere über eine Kopie:
# Eine Kopie anlegen, um sicher zu iterieren for byte in ba.copy(): process(byte)Zieh äußere Synchronisierung in Erwägung, wenn du
bytearray-Instanzen über Threads hinweg gemeinsam nutzt. Mehr dazu unter Unterstützung von freiem Threading in Python.
Threadsicherheit bei memoryview-Objekten¶
memoryview-Objekte bieten Zugriff auf die internen Daten eines zugrunde liegenden Objekts, ohne zu kopieren. Die Threadsicherheit hängt sowohl von der memoryview selbst als auch vom zugrunde liegenden Puffer-Exporteur ab.
Die Umsetzung von memoryview verwendet im Build mit freien Threads atomare Operationen, um ihre eigenen Exporte zu verfolgen. Eine memoryview zu erzeugen und freizugeben ist threadsicher. Der Attributzugriff (etwa shape, format) liest Felder, die für die Lebensdauer der memoryview unveränderlich sind. Gleichzeitige Lesezugriffe sind daher sicher, solange die memoryview nicht freigegeben wurde.
Die eigentlichen Daten, auf die über die memoryview zugegriffen wird, gehören allerdings dem zugrunde liegenden Objekt. Der gleichzeitige Zugriff auf diese Daten ist nur dann sicher, wenn das zugrunde liegende Objekt es zulässt:
Bei unveränderlichen Objekten wie
bytessind gleichzeitige Lesezugriffe über mehrere memoryviews sicher.Bei veränderbaren Objekten wie
bytearrayist es nicht sicher, denselben Speicherbereich aus mehreren Threads ohne äußere Synchronisierung zu lesen und zu schreiben. Das kann zu beschädigten Daten führen. Beachte, dass auch nur lesende memoryviews veränderbarer Objekte keine Datenkonflikte verhindern, wenn das zugrunde liegende Objekt von einem anderen Thread verändert wird.
# NICHT sicher: gleichzeitige Schreibzugriffe auf denselben Puffer
data = bytearray(1000)
view = memoryview(data)
# Thread 1: view[0:500] = b'x' * 500
# Thread 2: view[0:500] = b'y' * 500
# Sicher: eine Sperre für den gleichzeitigen Zugriff verwenden
import threading
lock = threading.Lock()
data = bytearray(1000)
view = memoryview(data)
with lock:
view[0:500] = b'x' * 500
Die Größe des zugrunde liegenden Objekts zu ändern oder es neu zuzuweisen (etwa durch einen Aufruf von bytearray.resize()), während eine memoryview exportiert ist, löst BufferError aus. Das wird unabhängig von Threads durchgesetzt.