C API-Erweiterungsunterstützung für Free Threading¶
Ab der Version 3.13 unterstützt CPython das Ausführen mit deaktiviertem global interpreter lock (GIL) in einer Konfiguration namens Free Threading. Dieses Dokument beschreibt, wie C API-Erweiterungen für die Unterstützung von Free Threading angepasst werden.
Identifizieren des Free-Threaded-Builds in C¶
Die CPython C API stellt das Makro Py_GIL_DISABLED bereit: Im Free-Threaded-Build ist es auf 1 definiert, im regulären Build ist es nicht definiert. Sie können es verwenden, um Code zu aktivieren, der nur im Free-Threaded-Build ausgeführt wird.
#ifdef Py_GIL_DISABLED
/* code that only runs in the free-threaded build */
#endif
Hinweis
Unter Windows ist dieses Makro nicht automatisch definiert, muss aber beim Erstellen an den Compiler übergeben werden. Die Funktion sysconfig.get_config_var() kann verwendet werden, um festzustellen, ob der aktuell laufende Interpreter das Makro definiert hatte.
Modulinitialisierung¶
Erweiterungsmodule müssen explizit angeben, dass sie die Ausführung mit deaktiviertem GIL unterstützen; andernfalls löst das Importieren der Erweiterung eine Warnung aus und aktiviert das GIL zur Laufzeit.
Es gibt zwei Möglichkeiten, anzugeben, dass ein Erweiterungsmodul die Ausführung mit deaktiviertem GIL unterstützt, je nachdem, ob die Erweiterung eine mehrphasige oder einphasige Initialisierung verwendet.
Mehrphasige Initialisierung¶
Erweiterungen, die eine mehrphasige Initialisierung verwenden (d. h. PyModuleDef_Init()), sollten einen Py_mod_gil-Slot in der Moduldefinition hinzufügen. Wenn Ihre Erweiterung ältere Versionen von CPython unterstützt, sollten Sie den Slot mit einer PY_VERSION_HEX-Prüfung schützen.
static struct PyModuleDef_Slot module_slots[] = {
...
#if PY_VERSION_HEX >= 0x030D0000
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
.m_slots = module_slots,
...
};
Einphasige Initialisierung¶
Erweiterungen, die eine einphasige Initialisierung verwenden (d. h. PyModule_Create()), sollten PyUnstable_Module_SetGIL() aufrufen, um anzugeben, dass sie die Ausführung mit deaktiviertem GIL unterstützen. Die Funktion ist nur im Free-Threaded-Build definiert, daher sollten Sie den Aufruf mit #ifdef Py_GIL_DISABLED schützen, um Kompilierungsfehler 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 API-Richtlinien¶
Die meisten C-APIs sind thread-sicher, es gibt jedoch einige Ausnahmen.
Struct-Felder: Der direkte Zugriff auf Felder in Python C API-Objekten oder -Structs ist nicht thread-sicher, wenn das Feld gleichzeitig geändert werden kann.
Makros: Zugriffs-Makros wie
PyList_GET_ITEM,PyList_SET_ITEMund Makros wiePySequence_Fast_GET_SIZE, die das vonPySequence_Fast()zurückgegebene Objekt verwenden, führen keine Fehlerprüfung oder Sperrung durch. Diese Makros sind nicht thread-sicher, wenn der Container gleichzeitig geändert werden kann.Ausgeliehene Referenzen: C API-Funktionen, die ausgeliehene Referenzen zurückgeben, sind möglicherweise nicht thread-sicher, wenn das enthaltende Objekt gleichzeitig geändert wird. Weitere Informationen finden Sie im Abschnitt über ausgeliehene Referenzen.
Thread-Sicherheit von Containern¶
Container wie PyListObject, PyDictObject und PySetObject führen im Free-Threaded-Build interne Sperrungen durch. Zum Beispiel sperrt PyList_Append() die Liste, bevor ein Element angehängt wird.
PyDict_Next¶
Eine bemerkenswerte Ausnahme ist PyDict_Next(), die das Dictionary nicht sperrt. Sie sollten Py_BEGIN_CRITICAL_SECTION verwenden, um das Dictionary beim Durchlaufen zu schützen, wenn das Dictionary gleichzeitig geä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();
Ausgeliehene Referenzen¶
Einige C API-Funktionen geben ausgeliehene Referenzen zurück. Diese APIs sind nicht thread-sicher, wenn das enthaltende Objekt gleichzeitig geändert wird. Zum Beispiel ist die Verwendung von PyList_GetItem() nicht sicher, wenn die Liste gleichzeitig geändert werden kann.
Die folgende Tabelle listet einige APIs für ausgeliehene Referenzen und ihre Ersetzungen auf, die starke Referenzen zurückgeben.
API für ausgeliehene Referenzen |
API für starke Referenzen |
|---|---|
keine (siehe PyDict_Next) |
|
Nicht alle APIs, die ausgeliehene Referenzen zurückgeben, sind problematisch. Zum Beispiel ist PyTuple_GetItem() sicher, da Tupel unveränderlich sind. Ebenso sind nicht alle Verwendungen der oben genannten APIs problematisch. Zum Beispiel wird PyDict_GetItem() oft zum Parsen von Keyword-Argument-Dictionaries in Funktionsaufrufen verwendet; diese Keyword-Argument-Dictionaries sind effektiv privat (nicht von anderen Threads zugänglich), daher ist die Verwendung von ausgeliehenen Referenzen in diesem Kontext sicher.
Einige dieser Funktionen wurden in Python 3.13 hinzugefügt. Sie können das Paket pythoncapi-compat verwenden, um Implementierungen dieser Funktionen für ältere Python-Versionen bereitzustellen.
Speicherallokations-APIs¶
Die C API für die Speicherverwaltung von Python bietet Funktionen in drei verschiedenen Allokationsdomänen: "raw", "mem" und "object". Für die Thread-Sicherheit erfordert der Free-Threaded-Build, dass nur Python-Objekte über die Objekt-Domäne allokiert werden und dass alle Python-Objekte über diese Domäne allokiert werden. Dies unterscheidet sich von früheren Python-Versionen, wo dies nur eine Best Practice und keine harte Anforderung war.
Hinweis
Suchen Sie nach Verwendungen von PyObject_Malloc() in Ihrer Erweiterung und prüfen Sie, ob der allokierte Speicher für Python-Objekte verwendet wird. Verwenden Sie PyMem_Malloc(), um Puffer zu allokieren, anstatt PyObject_Malloc().
Thread-Status- und GIL-APIs¶
Python bietet eine Reihe von Funktionen und Makros zur Verwaltung des Thread-Status und des GIL, wie z. B.
Diese Funktionen sollten im Free-Threaded-Build weiterhin verwendet werden, um den Thread-Status zu verwalten, auch wenn das GIL deaktiviert ist. Wenn Sie beispielsweise einen Thread außerhalb von Python erstellen, müssen Sie vor dem Aufruf der Python-API PyGILState_Ensure() aufrufen, um sicherzustellen, dass der Thread einen gültigen Python-Thread-Status hat.
Sie sollten weiterhin PyEval_SaveThread() oder Py_BEGIN_ALLOW_THREADS um blockierende Operationen wie I/O oder Sperrungsanforderungen herum aufrufen, um anderen Threads die Ausführung des zyklischen Garbage Collectors zu ermöglichen.
Schutz des internen Erweiterungszustands¶
Ihre Erweiterung kann über einen internen Zustand verfügen, der zuvor durch das GIL geschützt war. Möglicherweise müssen Sie Sperrungen hinzufügen, um diesen Zustand zu schützen. Der Ansatz hängt von Ihrer Erweiterung ab, aber einige gängige Muster sind
Caches: Globale Caches sind eine häufige Quelle für gemeinsamen Zustand. Erwägen Sie die Verwendung einer Sperrung zum Schutz des Caches oder dessen Deaktivierung im Free-Threaded-Build, wenn der Cache für die Leistung nicht entscheidend ist.
Globaler Zustand: Globaler Zustand muss möglicherweise durch eine Sperrung geschützt oder in den Thread-lokalen Speicher verschoben werden. C11 und C++11 bieten
thread_localoder_Thread_localfür Thread-lokalen Speicher.
Kritische Abschnitte¶
Im Free-Threaded-Build bietet CPython einen Mechanismus namens "kritische Abschnitte" zum Schutz von Daten, die sonst durch das GIL geschützt würden. Obwohl Erweiterungsautoren möglicherweise nicht direkt mit der internen Implementierung kritischer Abschnitte interagieren, ist das Verständnis ihres Verhaltens entscheidend bei der Verwendung bestimmter C API-Funktionen oder der Verwaltung von gemeinsam genutztem Zustand im Free-Threaded-Build.
Was sind kritische Abschnitte?¶
Konzeptionell fungieren kritische Abschnitte als eine Deadlock-Vermeidungsschicht, die auf einfachen Mutexes aufbaut. Jeder Thread verwaltet einen Stapel aktiver kritischer Abschnitte. Wenn ein Thread versucht, eine Sperrung zu erwerben, die mit einem kritischen Abschnitt verbunden ist (z. B. implizit beim Aufruf einer thread-sicheren C API-Funktion wie PyDict_SetItem() oder explizit über Makros), versucht er, den zugrunde liegenden Mutex zu erwerben.
Verwendung von kritischen Abschnitten¶
Die primären APIs zur Verwendung kritischer Abschnitte sind
Py_BEGIN_CRITICAL_SECTIONundPy_END_CRITICAL_SECTION- Zum Sperren eines einzelnen ObjektsPy_BEGIN_CRITICAL_SECTION2undPy_END_CRITICAL_SECTION2- Zum gleichzeitigen Sperren zweier Objekte
Diese Makros müssen in passenden Paaren verwendet werden und im selben C-Scope erscheinen, da sie einen neuen lokalen Scope definieren. Diese Makros sind in nicht-Free-Threaded-Builds wirkungslos, sodass sie sicher zu Code hinzugefügt werden können, der beide Build-Typen unterstützen muss.
Eine häufige Verwendung eines kritischen Abschnitts wäre das Sperren eines Objekts beim Zugriff auf ein internes Attribut davon. Wenn beispielsweise ein Erweiterungstyp ein internes Zählerfeld hat, könnten Sie einen kritischen Abschnitt verwenden, während Sie dieses Feld lesen oder schreiben.
// 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 funktionieren¶
Im Gegensatz zu herkömmlichen Sperren garantieren kritische Abschnitte keinen exklusiven Zugriff während ihrer gesamten Dauer. Wenn ein Thread blockieren würde, während er einen kritischen Abschnitt hält (z. B. durch Erwerb einer anderen Sperre oder Durchführung von I/O), wird der kritische Abschnitt vorübergehend ausgesetzt - alle Sperren werden freigegeben - und dann fortgesetzt, wenn die blockierende Operation abgeschlossen ist.
Dieses Verhalten ähnelt dem, was mit dem GIL geschieht, wenn ein Thread einen blockierenden Aufruf tätigt. Die Hauptunterschiede sind
Kritische Abschnitte arbeiten pro Objekt und nicht global
Kritische Abschnitte folgen einer Stack-Disziplin innerhalb jedes Threads (die "begin" und "end" Makros erzwingen dies, da sie gepaart und im selben Scope sein müssen)
Kritische Abschnitte geben Sperren automatisch frei und erwerben sie um potenzielle blockierende Operationen herum wieder.
Vermeidung von Deadlocks¶
Kritische Abschnitte helfen, Deadlocks auf zwei Arten zu vermeiden
Wenn ein Thread versucht, eine Sperre zu erwerben, die bereits von einem anderen Thread gehalten wird, setzt er zuerst alle seine aktiven kritischen Abschnitte aus und gibt deren Sperren vorübergehend frei.
Wenn die blockierende Operation abgeschlossen ist, wird nur der oberste kritische Abschnitt zuerst wiedererworben.
Das bedeutet, dass Sie sich nicht auf verschachtelte kritische Abschnitte verlassen können, um mehrere Objekte gleichzeitig zu sperren, da der innere kritische Abschnitt die äußeren aussetzen kann. Verwenden Sie stattdessen Py_BEGIN_CRITICAL_SECTION2, um zwei Objekte gleichzeitig zu sperren.
Beachten Sie, dass die oben genannten Sperren nur PyMutex-basierte Sperren sind. Die Implementierung kritischer Abschnitte kennt andere verwendete Sperrmechanismen wie POSIX-Mutexes nicht und beeinflusst diese nicht. Beachten Sie auch, dass, obwohl das Blockieren auf jedem PyMutex dazu führt, dass kritische Abschnitte ausgesetzt werden, nur die Mutexes freigegeben werden, die Teil der kritischen Abschnitte sind. Wenn PyMutex ohne kritischen Abschnitt verwendet wird, wird es nicht freigegeben und erhält daher keine gleiche Deadlock-Vermeidung.
Wichtige Überlegungen¶
Kritische Abschnitte können ihre Sperren vorübergehend freigeben und anderen Threads erlauben, die geschützten Daten zu ändern. Seien Sie vorsichtig bei Annahmen über den Zustand der Daten nach Operationen, die blockieren könnten.
Da Sperren vorübergehend freigegeben (ausgesetzt) werden können, garantiert der Eintritt in einen kritischen Abschnitt nicht den exklusiven Zugriff auf die geschützte Ressource während der gesamten Dauer des Abschnitts. Wenn Code innerhalb eines kritischen Abschnitts eine andere Funktion aufruft, die blockiert (z. B. eine andere Sperre erwirbt, blockierende I/O durchführt), werden alle Sperren, die der Thread über kritische Abschnitte hält, freigegeben. Dies ähnelt der Art und Weise, wie das GIL während blockierender Aufrufe freigegeben werden kann.
Nur die Sperre(n), die mit dem zuletzt eingegebenen (obersten) kritischen Abschnitt verbunden sind, sind garantiert zu jedem Zeitpunkt gehalten. Sperren für äußere, verschachtelte kritische Abschnitte können ausgesetzt worden sein.
Mit diesen APIs können Sie maximal zwei Objekte gleichzeitig sperren. Wenn Sie mehr Objekte sperren müssen, müssen Sie Ihren Code umstrukturieren.
Obwohl kritische Abschnitte nicht zu Deadlocks führen, wenn Sie versuchen, dasselbe Objekt zweimal zu sperren, sind sie für diesen Anwendungsfall weniger effizient als speziell entwickelte reentrente Sperren.
Bei der Verwendung von
Py_BEGIN_CRITICAL_SECTION2beeinflusst die Reihenfolge der Objekte nicht die Korrektheit (die Implementierung kümmert sich um die Deadlock-Vermeidung), aber es ist eine gute Praxis, Objekte immer in einer konsistenten Reihenfolge zu sperren.Denken Sie daran, dass die Makros für kritische Abschnitte hauptsächlich zum Schutz des Zugriffs auf *Python-Objekte* dienen, die an internen CPython-Operationen beteiligt sein könnten, die den oben beschriebenen Deadlock-Szenarien ausgesetzt sind. Zum Schutz rein interner Erweiterungszustände können Standard-Mutexes oder andere Synchronisationsprimitive geeigneter sein.
Erweiterungen für den Free-Threaded-Build erstellen¶
C API-Erweiterungen müssen speziell für den Free-Threaded-Build erstellt werden. Die Wheels, Shared Libraries und Binärdateien werden durch ein Suffix t gekennzeichnet.
pypa/manylinux unterstützt den Free-Threaded-Build mit dem Suffix
t, wie z. B.python3.13t.pypa/cibuildwheel unterstützt den Free-Threaded-Build, wenn Sie CIBW_ENABLE auf cpython-freethreading setzen.
Begrenzte C-API und stabile ABI¶
Der Free-Threaded-Build unterstützt derzeit nicht die Begrenzte C-API oder die stabile ABI. Wenn Sie setuptools zum Erstellen Ihrer Erweiterung verwenden und derzeit py_limited_api=True setzen, können Sie py_limited_api=not sysconfig.get_config_var("Py_GIL_DISABLED") verwenden, um die begrenzte API beim Erstellen mit dem Free-Threaded-Build zu deaktivieren.
Hinweis
Sie müssen separate Wheels speziell für den Free-Threaded-Build erstellen. Wenn Sie derzeit die stabile ABI verwenden, können Sie weiterhin ein einzelnes Wheel für mehrere Nicht-Free-Threaded-Python-Versionen erstellen.
Windows¶
Aufgrund einer Einschränkung des offiziellen Windows-Installers müssen Sie Py_GIL_DISABLED=1 manuell definieren, wenn Sie Erweiterungen aus dem Quellcode erstellen.
Siehe auch
Porting Extension Modules to Support Free-Threading: Ein von der Community gepflegter Portierungsleitfaden für Erweiterungsautoren.