Unterstützung von freiem Threading in Python¶
Seit der Veröffentlichung von 3.13 unterstützt CPython einen Python-Build namens freies Threading, bei dem der Global Interpreter Lock (GIL) abgeschaltet ist. Die freie Ausführung von Threads erlaubt es, die verfügbare Rechenleistung voll auszuschöpfen, indem Threads parallel auf den vorhandenen CPU-Kernen laufen. Nicht jede Software profitiert davon automatisch, aber Programme, die von Anfang an auf Threads ausgelegt sind, laufen auf Mehrkern-Hardware schneller.
Manche Pakete von Dritten, insbesondere solche mit einem Erweiterungsmodul, sind für den Einsatz in einem Free-Threaded-Build möglicherweise noch nicht bereit und schalten den GIL wieder ein.
Dieses Dokument beschreibt, was freies Threading für Python-Code bedeutet. Wie du C-Erweiterungen schreibst, die den Free-Threaded-Build unterstützen, steht unter Unterstützung von freiem Threading in C-API-Erweiterungen.
Siehe auch
PEP 703 – Making the Global Interpreter Lock Optional in CPython, für eine Gesamtbeschreibung von Python mit freiem Threading.
Installation¶
Seit Python 3.13 können die offiziellen Installationsprogramme für macOS und Windows auf Wunsch auch die Free-Threaded-Programmdateien von Python installieren. Die Installationsprogramme findest du unter https://www.python.org/downloads/.
Informationen zu anderen Plattformen findest du unter Installing a Free-Threaded Python, einer von der Gemeinschaft gepflegten Anleitung zur Installation von Python mit freiem Threading.
Wenn du CPython aus den Quellen baust, solltest du die configure-Option --disable-gil verwenden, um einen Python-Interpreter mit freiem Threading zu bauen.
Python mit freiem Threading erkennen¶
Um zu prüfen, ob der aktuelle Interpreter freies Threading unterstützt: python -VV und sys.version enthalten „free-threading build“. Mit der neuen Funktion sys._is_gil_enabled() lässt sich prüfen, ob der GIL im laufenden Prozess tatsächlich abgeschaltet ist.
Über die Konfigurationsvariable sysconfig.get_config_var("Py_GIL_DISABLED") lässt sich feststellen, ob der Build freies Threading unterstützt. Ist die Variable auf 1 gesetzt, unterstützt der Build freies Threading. Das ist der empfohlene Weg für Entscheidungen, die von der Build-Konfiguration abhängen.
Der Global Interpreter Lock in Python mit freiem Threading¶
Free-Threaded-Builds von CPython können auf Wunsch zur Laufzeit auch mit eingeschaltetem GIL laufen – über die Umgebungsvariable PYTHON_GIL oder die Befehlszeilenoption -X gil.
Der GIL kann außerdem automatisch eingeschaltet werden, wenn ein C-API-Erweiterungsmodul importiert wird, das nicht ausdrücklich als „unterstützt freies Threading“ gekennzeichnet ist. In diesem Fall wird eine Warnung ausgegeben.
Neben der Dokumentation der einzelnen Pakete verfolgen die folgenden Webseiten, wie weit beliebte Pakete freies Threading unterstützen:
Threadsicherheit¶
The free-threaded build of CPython aims to provide similar thread-safety
behavior at the Python level to the default GIL-enabled build. Built-in
types like dict, list, and set use internal locks
to protect against concurrent modifications in ways that behave similarly to
the GIL. See Garantien zur Threadsicherheit for the guarantees provided by built-in types.
Bemerkung
Es ist empfehlenswert, wo möglich threading.Lock oder andere Synchronisationsprimitive zu verwenden, anstatt sich auf die internen Sperren der integrierten Typen zu verlassen.
Bekannte Einschränkungen¶
Dieser Abschnitt beschreibt bekannte Einschränkungen des Free-Threaded-Builds von CPython.
Unsterblichkeit¶
Im Free-Threaded-Build sind manche Objekte unsterblich. Unsterbliche Objekte werden nicht freigegeben, und ihre Referenzzähler werden nie verändert. Damit wird ein Gedränge um die Referenzzähler vermieden, das eine effiziente Skalierung über mehrere Threads verhindern würde.
Mit der Veröffentlichung von 3.14 ist die Unsterblichkeit beschränkt auf:
Codekonstanten: Zahlenliterale, Zeichenkettenliterale und Tupelliterale, die aus anderen Konstanten bestehen.
Zeichenketten, die mit
sys.intern()interniert wurden.
Frame-Objekte¶
Es ist nicht sicher, von einem Frame-Objekt aus auf frame.f_locals zuzugreifen, wenn dieser Frame gerade in einem anderen Thread ausgeführt wird. Der Interpreter kann dabei abstürzen.
Iteratoren¶
Es ist im Allgemeinen nicht threadsicher, aus mehreren Threads gleichzeitig auf dasselbe Iteratorobjekt zuzugreifen. Die Threads können doppelte oder fehlende Elemente sehen.
Leistung bei einem einzelnen Thread¶
Der Free-Threaded-Build hat beim Ausführen von Python-Code einen zusätzlichen Aufwand gegenüber dem Standard-Build mit eingeschaltetem GIL. Wie groß dieser Aufwand ist, hängt von der Arbeitslast und der Hardware ab. In der Benchmark-Sammlung pyperformance reicht der durchschnittliche Aufwand von etwa 1 % auf macOS aarch64 bis zu 8 % auf x86-64-Linux-Systemen.
Verhaltensänderungen¶
Dieser Abschnitt beschreibt Verhaltensänderungen von CPython im Free-Threaded-Build.
Kontextvariablen¶
Im Free-Threaded-Build ist das Flag thread_inherit_context standardmäßig auf wahr gesetzt. Dadurch starten Threads, die mit threading.Thread erzeugt werden, mit einer Kopie des Context() des Aufrufers von start(). Im Standard-Build mit eingeschaltetem GIL ist das Flag standardmäßig falsch, sodass Threads mit einem leeren Context() starten.
Warnungsfilter¶
Im Free-Threaded-Build ist das Flag context_aware_warnings standardmäßig auf wahr gesetzt. Im Standard-Build mit eingeschaltetem GIL ist das Flag standardmäßig falsch. Ist das Flag wahr, verwendet der Kontextmanager warnings.catch_warnings eine Kontextvariable für die Warnungsfilter. Ist das Flag falsch, verändert catch_warnings die globale Filterliste, was nicht threadsicher ist. Näheres steht beim Modul warnings.
Höherer Speicherverbrauch¶
Der Free-Threaded-Build verbraucht gegenüber dem Standard-Build üblicherweise mehr Speicher. Dafür gibt es mehrere Gründe, die größtenteils auf Entwurfsentscheidungen zurückgehen.
Alle internierten Zeichenketten sind unsterblich¶
In modernen Python-Versionen (seit Version 2.3) macht das Internieren einer Zeichenkette (etwa mit sys.intern()) diese nicht unsterblich. Verschwindet stattdessen die letzte Referenz auf diese Zeichenkette, wird sie aus der Tabelle der internierten Zeichenketten entfernt. Im Free-Threaded-Build ist das nicht so: Jede internierte Zeichenkette wird unsterblich und überlebt bis zum Herunterfahren des Interpreters.
Objekte außerhalb der Speicherbereinigung haben einen größeren Objektkopf¶
Der Free-Threaded-Build verwendet eine andere PyObject-Struktur. Anstatt die Informationen zur Speicherbereinigung vor der PyObject-Struktur anzulegen, wie im Standard-Build, sind diese Informationen Teil des normalen Objektkopfs. Auf der AMD64-Plattform belegt None im Free-Threaded-Build zum Beispiel 32 Bytes gegenüber 16 Bytes im Standard-Build. Objekte, die der Speicherbereinigung unterliegen (etwa Dicts und Listen), sind in beiden Builds gleich groß, weil der Free-Threaded-Build für die Informationen zur Speicherbereinigung keinen zusätzlichen Platz braucht.
QSBR kann die Freigabe von Speicher verzögern¶
Um sperrfreie Datenstrukturen sicher umzusetzen, wird ein Verfahren zur sicheren Speicherrückgewinnung (safe memory reclamation, SMR) eingesetzt, bekannt als quiescent state-based reclamation (QSBR). Das bedeutet, dass der Speicher hinter Datenstrukturen mit sperrfreiem Zugriff QSBR verwendet, was die Freigabe aufschiebt, anstatt den Speicher sofort freizugeben. Zwei Beispiele für solche Datenstrukturen sind das Listenobjekt und das Objekt mit den Schlüsseln eines Wörterbuchs. Näheres dazu, wie QSBR umgesetzt ist, steht in InternalDocs/qsbr.md im Quellbaum von CPython. Ein Aufruf von gc.collect() sollte dazu führen, dass der ganze von QSBR gehaltene Speicher tatsächlich freigegeben wird. Beachte, dass die darunterliegende Speicherverwaltung diesen Speicher selbst dann, wenn QSBR ihn freigibt, möglicherweise nicht sofort an das Betriebssystem zurückgibt. Die Resident Set Size (RSS) des Prozesses sinkt dann also nicht.
Die Speicherverwaltung mimalloc gegenüber pymalloc¶
Der Standard-Build verwendet für kleine Speicheranforderungen (512 Bytes oder kleiner) normalerweise die Speicherverwaltung „pymalloc“. Der Free-Threaded-Build verwendet pymalloc nicht und legt alle Python-Objekte mit der Speicherverwaltung „mimalloc“ an. Die Speicherverwaltung pymalloc hat die folgenden Eigenschaften, die den Speicherverbrauch niedrig halten: geringer Aufwand je angelegtem Block, wirksame Vermeidung von Speicherfragmentierung und schnelle Rückgabe freien Speichers an das Betriebssystem. Die Speicherverwaltung mimalloc schlägt sich in diesen Punkten ebenfalls recht gut, kann aber etwas mehr Aufwand mitbringen.
Im Free-Threaded-Build verwaltet mimalloc den Speicher in mehreren getrennten Halden (derzeit vier). So werden zum Beispiel alle Objekte, die die Speicherbereinigung unterstützen, aus ihrer eigenen Halde angelegt. Getrennte Halden bedeuten, dass freier Speicher in einer Halde nicht für eine Anforderung aus einer anderen Halde genutzt werden kann. Außerdem sind manche Halden so eingerichtet, dass sie QSBR (quiescent-state based reclamation) verwenden, wenn der Speicher freigegeben wird, der die Halde unterlegt (in der Begriffswelt von mimalloc „Pages“ genannt). Durch den Einsatz von QSBR entsteht eine Verzögerung zwischen der Freigabe aller Speicherblöcke einer Page und der Freigabe der Speicherpage selbst – entweder für neue Anforderungen oder zurück an das Betriebssystem.
Auch die Speicherverwaltung mimalloc schiebt die Rückgabe freigegebenen Speichers an das Betriebssystem auf. Du kannst diese Verzögerung verkürzen, indem du die Umgebungsvariable MIMALLOC_PURGE_DELAY auf 0 setzt. Beachte, dass das die Leistung der Speicherverwaltung wahrscheinlich verschlechtert.
Referenzzählung mit freiem Threading kann Objekte länger leben lassen¶
Im Standard-Build wird ein Objekt normalerweise freigegeben, sobald sein Referenzzähler null erreicht. Der Free-Threaded-Build verwendet „biased reference counting“ mit einem schnellen Weg für Objekte, die dem aktuellen Thread „gehören“, und einem langsamen Weg für andere Objekte. Weitere Details stehen in PEP 703. Immer wenn der Referenzzähler eines Objekts in einem „eingereihten“ Zustand landet, kann die Freigabe aufgeschoben werden. Der eingereihte Zustand wird im Abschnitt „eval breaker“ des Bytecode-Auswerters aufgelöst.
Der Free-Threaded-Build erlaubt außerdem eine andere Art der Referenzzählung, bekannt als „deferred reference counting“ (aufgeschobene Referenzzählung). Diese Betriebsart wird pro Objekt über ein Flag eingeschaltet. Für die folgenden Typen ist die aufgeschobene Referenzzählung eingeschaltet:
Modulobjekte
Funktionen auf der obersten Ebene eines Moduls
Klassenmethoden, die im Klassenbereich definiert sind
Deskriptor-Objekte
thread-lokale Objekte, erzeugt mit
threading.local
Ist die aufgeschobene Referenzzählung eingeschaltet, werden Referenzen von den Stapeln von Python-Funktionen nicht in den Referenzzähler aufgenommen. Dieses Verfahren verringert den Aufwand der Referenzzählung, besonders für Objekte, die aus mehreren Threads verwendet werden. Weil die Referenzen auf dem Stapel nicht gezählt werden, werden Objekte mit aufgeschobener Referenzzählung nicht sofort freigegeben, wenn ihr interner Referenzzähler null erreicht. Sie werden stattdessen beim nächsten Lauf der Speicherbereinigung untersucht und freigegeben, wenn sich keine Referenzen auf dem Stapel mehr auf sie finden. Das heißt, dass diese Objekte von der Speicherbereinigung freigegeben werden und nicht, wie üblich, sobald ihr Referenzzähler null erreicht.
Referenzzählung pro Thread kann die Freigabe von Objekten verzögern¶
Um ein Gedränge um die Referenzzählerfelder häufig gemeinsam genutzter Objekte zu vermeiden, verwendet der Free-Threaded-Build für einige ausgewählte Objekttypen außerdem eine „Referenzzählung pro Thread“. Anstatt einen einzigen gemeinsamen Referenzzähler zu aktualisieren, führt jeder Thread sein eigenes lokales Feld von Referenzzählern, indiziert über eine dem Objekt zugewiesene eindeutige Kennung. Der tatsächliche Referenzzähler wird erst dann berechnet, wenn der lokale Zähler des Objekts null erreicht – durch Summieren der Zähler aller Threads. Die Referenzzählung pro Thread wird derzeit verwendet für:
Haldentypobjekte (in Python erzeugte Klassen)
Codeobjekte
das
__dict__von Modulobjekten
Weil die Zähler aller Threads vor der Freigabe des Objekts wieder zusammengeführt werden müssen, werden Objekte mit Referenzzählung pro Thread üblicherweise später freigegeben als im Standard-Build. Insbesondere wird ein solches Objekt meist erst dann freigegeben, wenn der Thread, der es referenziert hat, einen sicheren Punkt erreicht (zum Beispiel im Abschnitt „eval breaker“ des Bytecode-Auswerters) oder sich beendet. Ein Aufruf von gc.collect() führt die Zähler aller Threads zusammen und erlaubt die Freigabe dieser Objekte.