Unterstützung von freiem Threading in C-API-Erweiterungen¶
Seit der Veröffentlichung von 3.13 kann CPython mit abgeschaltetem Global Interpreter Lock (GIL) laufen – in einer Konfiguration namens freies Threading. Dieses Dokument beschreibt, wie du C-API-Erweiterungen anpasst, damit sie freies Threading unterstützen.
Den Free-Threaded-Build in C erkennen¶
Die C-API von CPython stellt das Makro Py_GIL_DISABLED bereit: Im Free-Threaded-Build ist es auf 1 definiert, im regulären Build ist es nicht definiert. Du kannst es verwenden, um Code einzuschalten, der nur im Free-Threaded-Build läuft:
#ifdef Py_GIL_DISABLED
/* code that only runs in the free-threaded build */
#endif
Bemerkung
Unter Windows wird dieses Makro nicht automatisch definiert, sondern muss dem Compiler beim Bauen angegeben werden. Mit der Funktion sysconfig.get_config_var() lässt sich feststellen, ob das Makro für den gerade laufenden Interpreter definiert war.
Modulinitialisierung¶
Erweiterungsmodule müssen ausdrücklich angeben, dass sie den Betrieb mit abgeschaltetem GIL unterstützen. Andernfalls löst der Import der Erweiterung eine Warnung aus und schaltet den GIL zur Laufzeit ein.
Es gibt zwei Wege, anzugeben, dass ein Erweiterungsmodul den Betrieb mit abgeschaltetem GIL unterstützt – je nachdem, ob die Erweiterung eine mehrphasige oder eine einphasige Initialisierung verwendet.
Mehrphasige Initialisierung¶
Erweiterungen, die eine mehrphasige Initialisierung verwenden (Funktionen wie PyModuleDef_Init(), der Export-Haken PyModExport_*, PyModule_FromSlotsAndSpec()), sollten in der Moduldefinition einen Slot Py_mod_gil ergänzen. Unterstützt deine Erweiterung auch ältere CPython-Versionen, solltest du den Slot mit einer Prüfung auf PY_VERSION_HEX absichern.
static struct PyModuleDef_Slot module_slots[] = {
...
#if PY_VERSION_HEX >= 0x030D0000
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
Einphasige Initialisierung¶
Erweiterungen, die die alte einphasige Initialisierung verwenden (also PyModule_Create()), sollten PyUnstable_Module_SetGIL() aufrufen, um anzugeben, dass sie den Betrieb mit abgeschaltetem GIL unterstützen. Die Funktion ist nur im Free-Threaded-Build definiert. Du solltest den Aufruf daher mit #ifdef Py_GIL_DISABLED absichern, um Kompilierfehler im regulären Build zu vermeiden.
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
...
};
PyMODINIT_FUNC
PyInit_mymodule(void)
{
PyObject *m = PyModule_Create(&moduledef);
if (m == NULL) {
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
}
Allgemeine Richtlinien zur API¶
Der größte Teil der C-API ist threadsicher, es gibt aber einige Ausnahmen.
Strukturfelder: Der direkte Zugriff auf Felder in Objekten oder Strukturen der Python-C-API ist nicht threadsicher, wenn das Feld gleichzeitig verändert werden kann.
Makros: Zugriffsmakros wie
PyList_GET_ITEMundPyList_SET_ITEMsowie Makros wiePySequence_Fast_GET_SIZE, die das vonPySequence_Fast()zurückgegebene Objekt verwenden, führen keine Fehlerprüfung und keine Sperrung durch. Diese Makros sind nicht threadsicher, wenn das Containerobjekt gleichzeitig verändert werden kann.Entlehnte Referenzen: C-API-Funktionen, die entlehnte Referenzen zurückgeben, sind möglicherweise nicht threadsicher, wenn das umgebende Objekt gleichzeitig verändert wird. Näheres steht im Abschnitt über entlehnte Referenzen.
Threadsicherheit von Containern¶
Container wie PyListObject, PyDictObject und PySetObject sperren im Free-Threaded-Build intern. So sperrt zum Beispiel PyList_Append() die Liste, bevor ein Element angehängt wird.
PyDict_Next¶
Eine bemerkenswerte Ausnahme ist PyDict_Next(), das das Wörterbuch nicht sperrt. Du solltest Py_BEGIN_CRITICAL_SECTION verwenden, um das Wörterbuch beim Iterieren zu schützen, wenn es gleichzeitig verändert werden kann:
Py_BEGIN_CRITICAL_SECTION(dict);
PyObject *key, *value;
Py_ssize_t pos = 0;
while (PyDict_Next(dict, &pos, &key, &value)) {
...
}
Py_END_CRITICAL_SECTION();
Entlehnte Referenzen¶
Manche C-API-Funktionen geben entlehnte Referenzen zurück. Diese APIs sind nicht threadsicher, wenn das umgebende Objekt gleichzeitig verändert wird. So ist es zum Beispiel nicht sicher, PyList_GetItem() zu verwenden, wenn die Liste gleichzeitig verändert werden kann.
Die folgende Tabelle führt einige APIs mit entlehnten Referenzen und ihren Ersatz auf, der starke Referenzen zurückgibt.
API mit entlehnter Referenz |
API mit starker Referenz |
|---|---|
keine (siehe PyDict_Next) |
|
|
|
|
|
Nicht alle APIs, die entlehnte Referenzen zurückgeben, sind problematisch. So ist zum Beispiel PyTuple_GetItem() sicher, weil Tupel unveränderlich sind. Ebenso sind nicht alle Verwendungen der obigen APIs problematisch. PyDict_GetItem() wird zum Beispiel oft verwendet, um Wörterbücher mit Schlüsselwortargumenten in Funktionsaufrufen zu zerlegen. Diese Wörterbücher sind praktisch privat (für andere Threads nicht zugänglich), sodass entlehnte Referenzen in diesem Zusammenhang sicher sind.
Einige dieser Funktionen wurden in Python 3.13 ergänzt. Du kannst das Paket pythoncapi-compat verwenden, um Implementierungen dieser Funktionen für ältere Python-Versionen zu erhalten.
APIs zur Speicherverwaltung¶
Die C-API von Python zur Speicherverwaltung stellt Funktionen in drei verschiedenen Anforderungsbereichen bereit: „raw“, „mem“ und „object“. Aus Gründen der Threadsicherheit verlangt der Free-Threaded-Build, dass nur Python-Objekte über den object-Bereich angelegt werden und dass alle Python-Objekte über diesen Bereich angelegt werden. Das unterscheidet sich von früheren Python-Versionen, in denen das nur eine Empfehlung und keine harte Anforderung war.
Bemerkung
Suche in deiner Erweiterung nach Verwendungen von PyObject_Malloc() und prüfe, ob der angeforderte Speicher für Python-Objekte verwendet wird. Verwende PyMem_Malloc() anstelle von PyObject_Malloc(), um Puffer anzufordern.
APIs für Thread-Zustand und GIL¶
Python bietet eine Reihe von Funktionen und Makros, um den Thread-Zustand und den GIL zu verwalten, etwa:
Diese Funktionen sollten auch im Free-Threaded-Build verwendet werden, um den Thread-Zustand zu verwalten, selbst wenn der GIL abgeschaltet ist. Erzeugst du zum Beispiel einen Thread außerhalb von Python, musst du PyThreadState_Ensure() aufrufen, bevor du in die Python-API hineinrufst, damit der Thread einen gültigen Python-Thread-Zustand hat.
Du solltest PyEval_SaveThread() oder Py_BEGIN_ALLOW_THREADS weiterhin um blockierende Vorgänge wie Ein- und Ausgabe oder das Anfordern von Sperren herum aufrufen, damit andere Threads die zyklische Garbage Collection ausführen können.
Internen Zustand einer Erweiterung schützen¶
Deine Erweiterung hat möglicherweise internen Zustand, der früher durch den GIL geschützt war. Vielleicht musst du Sperren ergänzen, um diesen Zustand zu schützen. Wie das aussieht, hängt von deiner Erweiterung ab, aber einige verbreitete Muster sind:
Zwischenspeicher: Globale Zwischenspeicher sind eine häufige Quelle gemeinsamen Zustands. Erwäge, den Zwischenspeicher mit einer Sperre zu schützen oder ihn im Free-Threaded-Build abzuschalten, wenn er für die Leistung nicht entscheidend ist.
Globaler Zustand: Globaler Zustand muss möglicherweise mit einer Sperre geschützt oder in thread-lokalen Speicher verschoben werden. C11 und C++11 bieten
thread_localbeziehungsweise_Thread_localfür thread-lokalen Speicher.
Kritische Abschnitte¶
Im Free-Threaded-Build bietet CPython einen Mechanismus namens „kritische Abschnitte“, um Daten zu schützen, die sonst durch den GIL geschützt wären. Autoren von Erweiterungen haben mit der internen Implementierung der kritischen Abschnitte zwar möglicherweise nie direkt zu tun, aber ihr Verhalten zu verstehen ist entscheidend, wenn du bestimmte C-API-Funktionen verwendest oder im Free-Threaded-Build gemeinsamen Zustand verwaltest.
Was sind kritische Abschnitte?¶
Vom Konzept her wirken kritische Abschnitte wie eine Schicht zur Verklemmungsvermeidung, die auf einfachen Mutexen aufsetzt. Jeder Thread führt einen Stapel der aktiven kritischen Abschnitte. Muss ein Thread eine Sperre anfordern, die zu einem kritischen Abschnitt gehört (etwa implizit beim Aufruf einer threadsicheren C-API-Funktion wie PyDict_SetItem() oder ausdrücklich über die Makros), versucht er, den darunterliegenden Mutex anzufordern.
Kritische Abschnitte verwenden¶
Die wichtigsten APIs für kritische Abschnitte sind:
Py_BEGIN_CRITICAL_SECTIONundPy_END_CRITICAL_SECTION– um ein einzelnes Objekt zu sperrenPy_BEGIN_CRITICAL_SECTION2undPy_END_CRITICAL_SECTION2– um zwei Objekte gleichzeitig zu sperren
Diese Makros müssen paarweise verwendet werden und im selben C-Gültigkeitsbereich stehen, weil sie einen neuen lokalen Gültigkeitsbereich eröffnen. In Builds ohne freies Threading tun diese Makros nichts, sie lassen sich also gefahrlos in Code ergänzen, der beide Build-Arten unterstützen muss.
Ein verbreiteter Einsatz eines kritischen Abschnitts ist, ein Objekt beim Zugriff auf eines seiner internen Attribute zu sperren. Hat ein Erweiterungstyp zum Beispiel ein internes Feld count, könntest du beim Lesen oder Schreiben dieses Feldes einen kritischen Abschnitt verwenden:
// read the count, returns new reference to internal count value
PyObject *result;
Py_BEGIN_CRITICAL_SECTION(obj);
result = Py_NewRef(obj->count);
Py_END_CRITICAL_SECTION();
return result;
// write the count, consumes reference from new_count
Py_BEGIN_CRITICAL_SECTION(obj);
obj->count = new_count;
Py_END_CRITICAL_SECTION();
Wie kritische Abschnitte arbeiten¶
Anders als herkömmliche Sperren sichern kritische Abschnitte nicht über ihre gesamte Dauer ausschließlichen Zugriff zu. Würde ein Thread blockieren, während er einen kritischen Abschnitt hält (etwa indem er eine weitere Sperre anfordert oder Ein- und Ausgabe betreibt), wird der kritische Abschnitt vorübergehend ausgesetzt – alle Sperren werden freigegeben – und nach Abschluss des blockierenden Vorgangs wieder aufgenommen.
Dieses Verhalten ähnelt dem, was beim GIL geschieht, wenn ein Thread einen blockierenden Aufruf macht. Die wesentlichen Unterschiede sind:
Kritische Abschnitte arbeiten je Objekt und nicht global
Kritische Abschnitte folgen innerhalb jedes Threads einer Stapeldisziplin (die Makros „begin“ und „end“ erzwingen das, weil sie paarweise und im selben Gültigkeitsbereich stehen müssen)
Kritische Abschnitte geben Sperren rund um möglicherweise blockierende Vorgänge automatisch frei und fordern sie danach wieder an
Verklemmungsvermeidung¶
Kritische Abschnitte helfen auf zwei Wege, Verklemmungen zu vermeiden:
Versucht ein Thread, eine Sperre anzufordern, die bereits ein anderer Thread hält, setzt er zunächst alle seine aktiven kritischen Abschnitte aus und gibt deren Sperren vorübergehend frei
Ist der blockierende Vorgang abgeschlossen, wird zuerst nur der oberste kritische Abschnitt wieder angefordert
Das heißt, dass du dich nicht darauf verlassen kannst, mit verschachtelten kritischen Abschnitten mehrere Objekte gleichzeitig zu sperren, weil der innere kritische Abschnitt die äußeren aussetzen kann. Verwende stattdessen Py_BEGIN_CRITICAL_SECTION2, um zwei Objekte gleichzeitig zu sperren.
Beachte, dass die oben beschriebenen Sperren ausschließlich Sperren auf Basis von PyMutex sind. Die Implementierung der kritischen Abschnitte weiß nichts von anderen möglicherweise verwendeten Sperrmechanismen, etwa POSIX-Mutexen, und beeinflusst sie nicht. Beachte außerdem: Das Blockieren an einem beliebigen PyMutex setzt die kritischen Abschnitte zwar aus, freigegeben werden aber nur die Mutexe, die Teil der kritischen Abschnitte sind. Wird ein PyMutex ohne kritischen Abschnitt verwendet, wird er nicht freigegeben und erhält daher nicht dieselbe Verklemmungsvermeidung.
Wichtige Überlegungen¶
Kritische Abschnitte können ihre Sperren vorübergehend freigeben und es anderen Threads so erlauben, die geschützten Daten zu verändern. Sei vorsichtig mit Annahmen über den Zustand der Daten nach Vorgängen, die blockieren könnten.
Weil Sperren vorübergehend freigegeben (ausgesetzt) werden können, sichert das Betreten eines kritischen Abschnitts über die gesamte Dauer des Abschnitts keinen ausschließlichen Zugriff auf die geschützte Ressource zu. Ruft Code innerhalb eines kritischen Abschnitts eine andere Funktion auf, die blockiert (etwa eine weitere Sperre anfordert oder blockierende Ein- und Ausgabe betreibt), werden alle Sperren freigegeben, die der Thread über kritische Abschnitte hält. Das ähnelt dem Umstand, dass der GIL während blockierender Aufrufe freigegeben werden kann.
Nur die Sperre beziehungsweise die Sperren des zuletzt betretenen (obersten) kritischen Abschnitts sind zu jedem Zeitpunkt garantiert gehalten. Die Sperren äußerer, verschachtelter kritischer Abschnitte können ausgesetzt sein.
Mit diesen APIs kannst du höchstens zwei Objekte gleichzeitig sperren. Musst du mehr Objekte sperren, musst du deinen Code umbauen.
Kritische Abschnitte verklemmen zwar nicht, wenn du versuchst, dasselbe Objekt zweimal zu sperren, sind für diesen Anwendungsfall aber weniger effizient als eigens dafür gebaute wiedereintrittsfähige Sperren.
Bei
Py_BEGIN_CRITICAL_SECTION2wirkt sich die Reihenfolge der Objekte nicht auf die Korrektheit aus (die Implementierung erledigt die Verklemmungsvermeidung), es ist aber gute Praxis, Objekte immer in derselben Reihenfolge zu sperren.Bedenke, dass die Makros für kritische Abschnitte vor allem dafür da sind, den Zugriff auf Python-Objekte zu schützen, die in interne CPython-Vorgänge mit den oben beschriebenen Verklemmungsgefahren verwickelt sein können. Um rein internen Zustand einer Erweiterung zu schützen, sind gewöhnliche Mutexe oder andere Synchronisationsprimitive möglicherweise passender.
Sperren je Objekt (ob_mutex)¶
Im Free-Threaded-Build enthält jedes Python-Objekt ein Feld ob_mutex vom Typ PyMutex. Dieser Mutex ist der Verwendung durch die API für kritische Abschnitte vorbehalten (Py_BEGIN_CRITICAL_SECTION / Py_END_CRITICAL_SECTION).
Warnung
Sperre ob_mutex nicht direkt mit PyMutex_Lock(&obj->ob_mutex). Direkte PyMutex_Lock-Aufrufe mit der API für kritische Abschnitte am selben Mutex zu mischen, kann Verklemmungen verursachen.
Selbst wenn dein eigener Code für einen bestimmten Objekttyp nie kritische Abschnitte verwendet, kann das Innere von CPython die API für kritische Abschnitte auf jedem Python-Objekt verwenden.
Braucht dein Erweiterungstyp eine eigene Sperre, ergänze in deiner Objektstruktur ein eigenes Feld vom Typ PyMutex (oder ein anderes Synchronisationsprimitiv). PyMutex ist sehr leichtgewichtig, ein weiterer kostet also so gut wie nichts.
Erweiterungen für den Free-Threaded-Build bauen¶
C-API-Erweiterungen müssen speziell für den Free-Threaded-Build gebaut werden. Die Wheels, dynamischen Bibliotheken und Programmdateien sind durch ein angehängtes t gekennzeichnet.
pypa/manylinux unterstützt den Free-Threaded-Build, mit angehängtem
t, etwapython3.14t.pypa/cibuildwheel unterstützt das Bauen von Wheels für den Free-Threaded-Build von Python 3.14 und neuer.
Eingeschränkte C-API und stabiles ABI¶
Der Free-Threaded-Build unterstützt derzeit weder die eingeschränkte C-API noch das stabile ABI. Wenn du deine Erweiterung mit setuptools baust und derzeit py_limited_api=True setzt, kannst du py_limited_api=not sysconfig.get_config_var("Py_GIL_DISABLED") verwenden, um beim Bauen mit dem Free-Threaded-Build auf die eingeschränkte API zu verzichten.
Bemerkung
Du musst eigene Wheels speziell für den Free-Threaded-Build bauen. Wenn du derzeit das stabile ABI verwendest, kannst du weiterhin ein einzelnes Wheel für mehrere Python-Versionen ohne freies Threading bauen.
Windows¶
Wegen einer Einschränkung des offiziellen Windows-Installationsprogramms musst du Py_GIL_DISABLED=1 beim Bauen von Erweiterungen aus den Quellen von Hand definieren.
Siehe auch
Porting Extension Modules to Support Free-Threading: eine von der Gemeinschaft gepflegte Portierungsanleitung für Autoren von Erweiterungen.