Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 788 – Schutz der C-API vor Interpreter-Finalisierung

Autor:
Peter Bierma <zintensitydev at gmail.com>
Sponsor:
Victor Stinner <vstinner at python.org>
Discussions-To:
Discourse thread
Status:
Entwurf
Typ:
Standards Track
Erstellt:
23. April 2025
Python-Version:
3.15
Post-History:
10. März 2025, 27. April 2025, 28. Mai 2025, 03. Okt 2025

Inhaltsverzeichnis

Zusammenfassung

Dieses PEP führt eine Reihe von Funktionen in der C-API ein, um sich sicher mit einem Interpreter zu verbinden, indem die Finalisierung verhindert wird. Zum Beispiel:

static int
thread_function(PyInterpreterView view)
{
    // Prevent the interpreter from finalizing
    PyInterpreterGuard guard = PyInterpreterGuard_FromView(view);
    if (guard == 0) {
        return -1;
    }

    // Analogous to PyGILState_Ensure(), but this is thread-safe.
    PyThreadView thread_view = PyThreadState_Ensure(guard);
    if (thread_view == 0) {
        PyInterpreterGuard_Close(guard);
        return -1;
    }

    // Now we can call Python code, without worrying about the thread
    // hanging due to finalization.
    if (PyRun_SimpleString("print('My hovercraft is full of eels') < 0) {
        PyErr_Print();
    }

    // Destroy the thread state and allow the interpreter to finalize.
    PyThreadState_Release(thread_view);
    PyInterpreterGuard_Close(guard);
    return 0;
}

Darüber hinaus sind die APIs der PyGILState-Familie durch diesen Vorschlag veraltet.

Hintergrund

In der C-API können Threads mit einem Interpreter interagieren, indem sie einen attached thread state für den aktuellen Thread halten. Dies kann bei der Erstellung und dem Anhängen von thread states auf sichere Weise kompliziert werden, da jeder Nicht-Python-Thread (einer, der nicht über das threading-Modul erstellt wurde) als "daemon" gilt, was bedeutet, dass der Interpreter auf diesen Thread nicht wartet, bevor er herunterfährt. Stattdessen hängt der Interpreter den Thread, wenn er versucht, einen Thread-State anzuhängen, was den Thread danach unbrauchbar macht.

Das Anhängen eines Thread-States kann jederzeit beim Aufruf von Python geschehen, z. B. zwischen Bytecode-Instruktionen (um den GIL an einen anderen Thread abzugeben) oder wenn eine C-Funktion einen Py_BEGIN_ALLOW_THREADS-Block verlässt. Daher reicht es nicht aus, nur gegen den Finalisierungsstatus des Interpreters zu prüfen, um Python-Code sicher aufzurufen. (Beachten Sie, dass das Hängen des Threads ein relativ neues Verhalten ist; in älteren Versionen würde der Thread beendet werden, aber das Problem ist dasselbe.)

Derzeit bietet die C-API keine Möglichkeit, sicherzustellen, dass ein Interpreter in einem Zustand ist, der keinen Thread beim Versuch des Anhängens hängen lässt. Dies kann ein frustrierendes Problem in großen Anwendungen sein, die Python-Code neben anderem nativen Code ausführen müssen.

Darüber hinaus verwenden Benutzer, die Nicht-Python-Threads erstellen, häufig PyGILState_Ensure(), das in PEP 311 eingeführt wurde. Dies war für Sub-Interpreter sehr unglücklich, da PyGILState_Ensure() dazu neigt, einen Thread-State für den Hauptinterpreter anstatt für den aktuellen Interpreter zu erstellen. Dies führt zu Thread-Sicherheits-Problemen, wenn Erweiterungen Threads erstellen, die mit dem Python-Interpreter interagieren, da Annahmen über den GIL falsch sind.

Motivation

Nicht-Python-Threads hängen immer während der Finalisierung

Viele große Bibliotheken müssen möglicherweise Python-Code in hochgradig asynchronen Situationen aufrufen, in denen der gewünschte Interpreter finalisiert oder gelöscht werden könnte, aber Code nach dem Aufruf des Interpreters weiter ausführen möchten. Dieser Wunsch wurde von Benutzern geäußert. Zum Beispiel könnte ein Callback, der Python-Code aufrufen möchte, aufgerufen werden, wenn:

  • Ein Kernel wurde auf einer GPU ausgeführt.
  • Ein Netzwerkpaket wurde empfangen.
  • Ein Thread wurde beendet, und eine native Bibliothek führt statische Finalisierer für Thread-lokalen Speicher aus.

Im Allgemeinen würde dieses Muster etwa so aussehen:

static void
some_callback(void *closure)
{
    /* Do some work */
    /* ... */

    PyGILState_STATE gstate = PyGILState_Ensure();
    /* Invoke the C API to do some computation */
    PyGILState_Release(gstate);

    /* ... */
}

Dies bedeutet, dass jeder Nicht-Python-Thread jederzeit beendet werden kann, was Benutzer, die mehr als nur Python-Code in ihrem Aufrufstrom ausführen möchten, stark einschränkt.

Py_IsFinalizing ist nicht atomar

Aufgrund des zuvor erwähnten Problems empfehlen die Dokumentation derzeit Py_IsFinalizing(), um die Beendigung des Threads zu verhindern.

Der Aufruf dieser Funktion von einem Thread aus, wenn die Laufzeit finalisiert, beendet den Thread, auch wenn der Thread nicht von Python erstellt wurde. Sie können Py_IsFinalizing() oder sys.is_finalizing() verwenden, um zu prüfen, ob der Interpreter gerade finalisiert wird, bevor Sie diese Funktion aufrufen, um unerwünschte Beendigung zu vermeiden.

Leider funktioniert dies aufgrund von Zeitproblemen zwischen Aufruf und Nutzung nicht zuverlässig. Der Interpreter könnte während des Aufrufs von Py_IsFinalizing() nicht finalisieren, aber er könnte sofort danach mit der Finalisierung beginnen, was das Anhängen eines Thread-States dazu bringen würde, den Thread hängen zu lassen.

Benutzer haben sich in der Vergangenheit einen Wunsch nach einer atomaren Möglichkeit geäußert, Py_IsFinalizing aufzurufen.

Locks in nativen Erweiterungen können während der Finalisierung unbrauchbar sein

Beim Erwerb von Sperren in einer nativen API ist es üblich, den GIL (oder kritische Abschnitte im Free-Threaded Build) freizugeben, um Lock-Ordering-Deadlocks zu vermeiden. Dies kann während der Finalisierung problematisch sein, da Threads, die Sperren halten, hängen können. Zum Beispiel:

  1. Ein Thread versucht, eine Sperre zu erwerben, und trennt zuerst seinen Thread-State, um Deadlocks zu vermeiden.
  2. Der Hauptthread beginnt mit der Finalisierung und weist alle Thread-States an, beim Anhängen zu hängen.
  3. Der Thread erwirbt die Sperre, auf die er gewartet hat, hängt aber dann beim Versuch, seinen Thread-State über Py_END_ALLOW_THREADS wieder anzuhängen.
  4. Der Hauptthread kann die Sperre nicht mehr erwerben, da der Thread, der sie hält, hängt.

Dies betrifft CPython selbst, und mit der aktuellen API kann nicht viel dagegen getan werden. Zum Beispiel bemerkt python/cpython#129536, dass das ssl-Modul einen fatalen Fehler ausgeben kann, wenn es während der Finalisierung verwendet wird, da ein Daemon-Thread beim Halten der Sperre für sys.stderr hing, und dann ein Finalisierer versuchte, darauf zu schreiben. Idealerweise sollte ein Thread temporär verhindern können, dass der Interpreter ihn hängt, während er die Sperre hält.

Das Finalisierungsverhalten für PyGILState_Ensure kann sich nicht ändern

Es wird immer einen Punkt in einem Python-Programm geben, an dem PyGILState_Ensure() keinen Thread-State mehr anhängen kann. Wenn der Interpreter längst tot ist, kann Python einem Thread offensichtlich keinen Weg geben, ihn aufzurufen. PyGILState_Ensure() hat keine sinnvolle Möglichkeit, einen Fehler zurückzugeben, daher bleibt ihm nichts anderes übrig, als den Thread zu beenden oder einen fatalen Fehler auszugeben, wie in python/cpython#124622 vermerkt.

Ich denke, eine neue C-API für die GIL-Erfassung und -Freigabe wäre erforderlich. Die Art und Weise, wie die bestehenden in vorhandenem C-Code verwendet werden, ist nicht dafür geeignet, plötzlich einen Fehlerstatus hinzuzufügen; kein vorhandener C-Code ist so geschrieben. Nach dem Aufruf gehen sie immer davon aus, dass sie die GIL haben und fortfahren können. Die API wurde als „sie blockiert und gibt erst zurück, wenn sie die GIL hat“ ohne andere Option entworfen.

Daher kann CPython keine wirklichen Änderungen an der Funktionsweise von PyGILState_Ensure() während der Finalisierung vornehmen, da dies bestehenden Code brechen würde.

Der Begriff „GIL“ ist knifflig für Free-Threading

Ein erhebliches Problem mit dem Begriff "GIL" in der C-API ist, dass er semantisch irreführend ist. Dies wurde in python/cpython#127989 vermerkt, erstellt vom Autor dieses PEP.

Das größte Problem ist, dass es beim Free-Threading keine GIL gibt, sodass Benutzer fälschlicherweise die C-API innerhalb von Py_BEGIN_ALLOW_THREADS-Blöcken aufrufen oder PyGILState_Ensure in neuen Threads weglassen.

Auch hier holt sich PyGILState_Ensure() einen angehängten Thread-State für den Thread sowohl bei With-GIL- als auch bei Free-Threaded-Builds. Ein angehängter Thread-State wird immer benötigt, um die C-API aufzurufen, daher muss PyGILState_Ensure() auch bei Free-Threaded-Builds aufgerufen werden, aber mit einem Namen wie „ensure GIL“ ist nicht sofort klar, dass dies zutrifft.

PyGILState_Ensure rät nicht den richtigen Interpreter

Wie in der Dokumentation erwähnt, sind die PyGILState-Funktionen in Sub-Interpretern nicht offiziell unterstützt.

Beachten Sie, dass die PyGILState_*-Funktionen davon ausgehen, dass es nur einen globalen Interpreter gibt (automatisch erstellt von Py_Initialize()). Python unterstützt die Erstellung zusätzlicher Interpreter (mit Py_NewInterpreter()), aber die Vermischung mehrerer Interpreter und der PyGILState_*-API ist nicht unterstützt.

Dies liegt daran, dass PyGILState_Ensure() keine Möglichkeit hat zu wissen, welcher Interpreter den Thread erstellt hat, und daher davon ausgehen muss, dass es sich um den Hauptinterpreter handelte. Es gibt keine Möglichkeit, dies zur Laufzeit zu erkennen, daher werden fehlerhafte Rennen in Threads, die von Sub-Interpretern erstellt wurden, zwangsläufig auftreten, da die Synchronisation für den falschen Interpreter für Objekte verwendet wird, die zwischen den Threads gemeinsam genutzt werden.

Wenn der Thread beispielsweise Zugriff auf Objekt A hätte, das zu einem Sub-Interpreter gehört, und dann PyGILState_Ensure() aufrufen würde, hätte der Thread einen angehängten Thread-State, der auf den Hauptinterpreter und nicht auf den Sub-Interpreter zeigt. Das bedeutet, dass alle GIL-Annahmen über das Objekt falsch sind, da es keine Synchronisation zwischen den beiden GILs gibt.

Es gibt keine gute Möglichkeit, dies zu lösen, außer eine neue API einzuführen, die explizit einen Interpreter vom Aufrufer annimmt.

Sub-Interpreter können gleichzeitig deallokiert werden

Die andere Möglichkeit, einen Nicht-Python-Thread zu erstellen, PyThreadState_New() und PyThreadState_Swap(), ist für die Unterstützung von Sub-Interpretern wesentlich besser (da PyThreadState_New() einen expliziten Interpreter annimmt, anstatt davon auszugehen, dass der Hauptinterpreter angefordert wurde), ist aber immer noch durch die aktuellen Hängeprobleme in der C-API eingeschränkt und anfällig für Abstürze, wenn der Sub-Interpreter vor Beginn des Threads finalisiert wird. Dies liegt daran, dass bei Sub-Interpretern die PyInterpreterState *-Struktur auf dem Heap zugewiesen wird, während der Hauptinterpreter statisch auf dem Python-Laufzeitstatus zugewiesen wird.

Begründung

Verhinderung des Interpreter-Shutdowns

Dieses PEP verfolgt einen Ansatz, bei dem ein Interpreter eine Guarding-API enthält, die verhindert, dass er heruntergefahren wird. Das Halten eines Interpreter-Guards stellt sicher, dass der Aufruf der C-API sicher ist, ohne sich darüber Gedanken machen zu müssen, dass der Thread durch die Finalisierung hängt.

Das bedeutet, dass die Interaktion mit Python (z. B. in einer C++-Bibliothek) einen Guard für den Interpreter benötigt, um das Objekt sicher aufzurufen, was umständlicher ist, als davon auszugehen, dass der Hauptinterpreter die richtige Wahl ist, aber es gibt wirklich keine andere Option.

Dieser Vorschlag kommt auch mit "Views" auf einen Interpreter, die verwendet werden können, um sicher auf einen Interpreter zuzugreifen, der tot oder lebendig sein kann. Mit einer View können Benutzer zu jedem Zeitpunkt seines Lebenszyklus einen Interpreter-Guard erstellen, und er wird sicher fehlschlagen, wenn der Interpreter keinen Python-Code mehr sicher ausführen kann.

Kompatibilitäts-Shim für PyGILState_Ensure

Dieser Vorschlag enthält PyUnstable_InterpreterView_FromDefault() als Kompatibilitäts-Hack für einige Benutzer von PyGILState_Ensure(). Es ist eine Thread-sichere Möglichkeit, einen Guard für den Haupt- (oder "Standard-") Interpreter zu erstellen.

Der größte Nachteil bei der Portierung von neuem Code auf PyThreadState_Ensure() ist, dass es kein Drop-in-Ersatz für PyGILState_Ensure() ist, da es ein Interpreter-Guard-Argument benötigt. In einigen großen Anwendungen kann Refactoring zur Verwendung eines PyInterpreterGuard überall schwierig sein, daher dient diese Funktion als letzter Ausweg für Benutzer, die explizit keine Unterstützung für Sub-Interpreter wünschen.

Spezifikation

Interpreter-Guards

type PyInterpreterGuard
Ein opaker Interpreter-Guard.

Durch das Halten eines Interpreter-Guards kann der Aufrufer sicherstellen, dass der Interpreter nicht finalisiert wird, bis der Guard zerstört ist.

Dies ähnelt einem "Readers-Writers"-Lock; Threads können einen Interpreter-Guard gleichzeitig halten, und der Interpreter muss warten, bis alle Threads ihre Guards zerstört haben, bevor er in die Finalisierung eintreten kann.

Dieser Typ ist garantiert zeigergroß.

PyInterpreterGuard PyInterpreterGuard_FromCurrent(void)
Erstellt einen Finalisierungs-Guard für den aktuellen Interpreter.

Bei Erfolg schützt diese Funktion den Interpreter und gibt eine opake Referenz auf den Guard zurück; bei Fehler gibt sie 0 mit einer gesetzten Ausnahme zurück.

Der Aufrufer muss einen attached thread state halten.

PyInterpreterGuard PyInterpreterGuard_FromView(PyInterpreterView view)
Erstellt einen Finalisierungs-Guard für einen Interpreter über eine View.

Bei Erfolg gibt diese Funktion einen Guard für den durch view dargestellten Interpreter zurück. Die View ist nach dem Aufruf dieser Funktion weiterhin gültig.

Wenn der Interpreter nicht mehr existiert oder keinen Python-Code mehr sicher ausführen kann, gibt diese Funktion 0 ohne Setzen einer Ausnahme zurück.

Der Aufrufer muss keinen attached thread state halten.

PyInterpreterState *PyInterpreterGuard_GetInterpreter(PyInterpreterGuard guard)
Gibt den von guard geschützten PyInterpreterState-Zeiger zurück.

Diese Funktion kann nicht fehlschlagen, und der Aufrufer muss keinen attached thread state halten.

PyInterpreterGuard PyInterpreterGuard_Copy(PyInterpreterGuard guard)
Dupliziert einen Interpreter-Guard.

Bei Erfolg gibt diese Funktion eine Kopie von guard zurück; bei Fehler gibt sie 0 ohne gesetzte Ausnahme zurück.

Der Aufrufer muss keinen attached thread state halten.

void PyInterpreterGuard_Close(PyInterpreterGuard guard)
Zerstört einen Interpreter-Guard und erlaubt dem Interpreter, in die Finalisierung einzutreten, wenn keine anderen Guards mehr vorhanden sind.

Diese Funktion kann nicht fehlschlagen, und der Aufrufer muss keinen attached thread state halten.

Interpreter-Views

type PyInterpreterView
Eine opake View eines Interpreters.

Dies ist eine Thread-sichere Möglichkeit, auf einen Interpreter zuzugreifen, der in einem anderen Thread finalisiert sein könnte.

Dieser Typ ist garantiert zeigergroß.

PyInterpreterView PyInterpreterView_FromCurrent(void)
Erstellt eine View für den aktuellen Interpreter.

Diese Funktion ist im Allgemeinen zur Verwendung in Verbindung mit PyInterpreterGuard_FromView() gedacht.

Bei Erfolg gibt diese Funktion eine View für den aktuellen Interpreter zurück; bei Fehler gibt sie 0 mit einer gesetzten Ausnahme zurück.

Der Aufrufer muss einen attached thread state halten.

PyInterpreterView PyInterpreterView_Copy(PyInterpreterView view)
Dupliziert eine View auf einen Interpreter.

Bei Erfolg gibt diese Funktion eine nicht-null Kopie von view zurück; bei Fehler gibt sie 0 ohne gesetzte Ausnahme zurück.

Diese Funktion kann nicht fehlschlagen, und der Aufrufer muss keinen attached thread state halten.

void PyInterpreterView_Close(PyInterpreterView view)
Löscht eine Interpreter-View.

Diese Funktion kann nicht fehlschlagen, und der Aufrufer muss keinen attached thread state halten.

PyInterpreterView PyUnstable_InterpreterView_FromDefault()
Erstellt eine View für einen beliebigen "Haupt"-Interpreter.

Diese Funktion existiert nur für Ausnahmefälle, in denen kein spezifischer Interpreter gespeichert werden kann.

Bei Erfolg gibt diese Funktion eine View für den Hauptinterpreter zurück; bei Fehler gibt sie 0 ohne gesetzte Ausnahme zurück.

Der Aufrufer muss keinen attached thread state halten.

Sicherstellen und Freigeben von Thread-States

Dieser Vorschlag enthält zwei neue High-Level-Threading-APIs, die PyGILState_Ensure() und PyGILState_Release() ersetzen sollen.

type PyThreadView
Eine opake View eines thread state.

In diesem PEP bietet eine Thread-View keine zusätzlichen Eigenschaften über einen PyThreadState*-Zeiger hinaus. APIs für PyThreadView können jedoch in Zukunft hinzugefügt werden.

Dieser Typ ist garantiert zeigergroß.

PyThreadView PyThreadState_Ensure(PyInterpreterGuard guard)
Stellt sicher, dass der Thread einen attached thread state für den von guard geschützten Interpreter hat und somit diesen Interpreter sicher aufrufen kann. Es ist in Ordnung, diese Funktion aufzurufen, wenn der Thread bereits einen angehängten Thread-State hat, solange es einen nachfolgenden Aufruf von PyThreadState_Release() gibt, der diesem entspricht.

Verschachtelte Aufrufe dieser Funktion erstellen nur manchmal einen neuen thread state. Wenn kein angehängter Thread-State vorhanden ist, prüft diese Funktion den zuletzt von diesem Thread verwendeten angehängten Thread-State. Wenn keiner existiert oder er nicht mit guard übereinstimmt, wird ein neuer Thread-State erstellt. Wenn er mit guard übereinstimmt, wird er wieder angehängt. Wenn ein angehängter Thread-State vorhanden ist, tritt eine ähnliche Prüfung auf; wenn der Interpreter mit guard übereinstimmt, wird er angehängt, andernfalls wird ein neuer Thread-State erstellt.

Gibt eine nicht-null Thread-View des alten Thread-States bei Erfolg zurück und 0 bei Fehler.

void PyThreadState_Release(PyThreadView view)
Gibt einen PyThreadState_Ensure()-Aufruf frei.

Der attached thread state vor dem entsprechenden PyThreadState_Ensure()-Aufruf wird garantiert wiederhergestellt, wenn die Funktion zurückkehrt. Der zwischengespeicherte Thread-State, wie er von PyThreadState_Ensure() und PyGILState_Ensure() verwendet wird (der "GIL-State"), wird ebenfalls wiederhergestellt.

Diese Funktion kann nicht fehlschlagen.

Veralterung von PyGILState-APIs

Dieses PEP veraltet alle bestehenden PyGILState-APIs zugunsten der bestehenden und neuen PyThreadState-APIs. Nämlich:

Alle PyGILState-APIs werden aus der nicht-limitierten C-API in Python 3.20 entfernt. Sie bleiben für Kompatibilität in der stabilen ABI verfügbar.

Abwärtskompatibilität

Dieses PEP spezifiziert eine Breaking-Change-Änderung mit der Entfernung aller PyGILState-APIs aus den öffentlichen Headern der nicht-limitierten C-API in Python 3.20.

Sicherheitsimplikationen

Dieses PEP hat keine bekannten Sicherheitsimplikationen.

Wie man das lehrt

Wie alle C-API-Funktionen werden alle neuen APIs in diesem PEP in der C-API-Dokumentation dokumentiert, idealerweise unter dem Abschnitt „Nicht von Python erstellte Threads“. Die vorhandene PyGILState-Dokumentation sollte entsprechend aktualisiert werden, um auf die neuen APIs zu verweisen.

Beispiele

Diese Beispiele sollen helfen, die in diesem PEP beschriebenen APIs zu verstehen. Sie könnten in der Dokumentation wiederverwendet werden.

Beispiel: Eine Bibliotheks-Schnittstelle

Stellen Sie sich vor, Sie entwickeln eine C-Bibliothek zum Protokollieren. Möglicherweise möchten Sie eine API anbieten, die es Benutzern ermöglicht, in ein Python-Dateiobjekt zu protokollieren.

Mit diesem PEP würden Sie es wie folgt implementieren:

int
LogToPyFile(PyInterpreterView view,
            PyObject *file,
            PyObject *text)
{
    PyInterpreterGuard guard = PyInterpreterGuard_FromView(view);
    if (guard == 0) {
        /* Python interpreter has shut down */
        return -1;
    }

    PyThreadView thread_view = PyThreadState_Ensure(guard);
    if (thread_view == 0) {
        PyInterpreterGuard_Close(guard);
        fputs("Cannot call Python.\n", stderr);
        return -1;
    }

    const char *to_write = PyUnicode_AsUTF8(text);
    if (to_write == NULL) {
        // Since the exception may be destroyed upon calling PyThreadState_Release(),
        // print out the exception ourselves.
        PyErr_Print();
        PyThreadState_Release(thread_view);
        PyInterpreterGuard_Close(guard);
        return -1;
    }
    int res = PyFile_WriteString(to_write, file);
    free(to_write);
    if (res < 0) {
        PyErr_Print();
    }

    PyThreadState_Release(thread_view);
    PyInterpreterGuard_Close(guard);
    return res < 0;
}

Beispiel: Ein Single-Threaded Ensure

Dieses Beispiel zeigt, wie ein C-Lock in einer von C definierten Python-Methode erworben wird.

Wenn dies von einem Daemon-Thread aufgerufen würde, könnte der Interpreter den Thread beim Wiederanhängen seines Thread-States hängen lassen, wodurch wir die Sperre gehalten haben. Jede zukünftige Finalisierung, die versucht, die Sperre zu erwerben, wäre in einer Deadlock-Situation.

static PyObject *
my_critical_operation(PyObject *self, PyObject *Py_UNUSED(args))
{
    assert(PyThreadState_GetUnchecked() != NULL);
    PyInterpreterGuard guard = PyInterpreterGuard_FromCurrent();
    if (guard == 0) {
        /* Python interpreter has shut down */
        return NULL;
    }

    Py_BEGIN_ALLOW_THREADS;
    acquire_some_lock();

    /* Do something while holding the lock.
       The interpreter won't finalize during this period. */
    // ...

    release_some_lock();
    Py_END_ALLOW_THREADS;
    PyInterpreterGuard_Close(guard);
    Py_RETURN_NONE;
}

Beispiel: Umstellung von den Legacy-Funktionen

Der folgende Code verwendet die PyGILState-APIs:

static int
thread_func(void *arg)
{
    PyGILState_STATE gstate = PyGILState_Ensure();
    /* It's not an issue in this example, but we just attached
       a thread state for the main interpreter. If my_method() was
       originally called in a subinterpreter, then we would be unable
       to safely interact with any objects from it. */
    if (PyRun_SimpleString("print(42)") < 0) {
        PyErr_Print();
    }
    PyGILState_Release(gstate);
    return 0;
}

static PyObject *
my_method(PyObject *self, PyObject *unused)
{
    PyThread_handle_t handle;
    PyThead_indent_t indent;

    if (PyThread_start_joinable_thread(thread_func, NULL, &ident, &handle) < 0) {
        return NULL;
    }
    Py_BEGIN_ALLOW_THREADS;
    PyThread_join_thread(handle);
    Py_END_ALLOW_THREADS;
    Py_RETURN_NONE;
}

Dies ist derselbe Code, neu geschrieben, um die neuen Funktionen zu verwenden:

static int
thread_func(void *arg)
{
    PyInterpreterGuard guard = (PyInterpreterGuard)arg;
    PyThreadView thread_view = PyThreadState_Ensure(guard);
    if (thread_view == 0) {
        PyInterpreterGuard_Close(guard);
        return -1;
    }
    if (PyRun_SimpleString("print(42)") < 0) {
        PyErr_Print();
    }
    PyThreadState_Release(thread_view);
    PyInterpreterGuard_Close(guard);
    return 0;
}

static PyObject *
my_method(PyObject *self, PyObject *unused)
{
    PyThread_handle_t handle;
    PyThead_indent_t indent;

    PyInterpreterGuard guard = PyInterpreterGuard_FromCurrent();
    if (guard == 0) {
        return NULL;
    }

    if (PyThread_start_joinable_thread(thread_func, (void *)guard, &ident, &handle) < 0) {
        PyInterpreterGuard_Close(guard);
        return NULL;
    }
    Py_BEGIN_ALLOW_THREADS
    PyThread_join_thread(handle);
    Py_END_ALLOW_THREADS
    Py_RETURN_NONE;
}

Beispiel: Ein Daemon-Thread

Mit diesem PEP sind Daemon-Threads sehr ähnlich wie Nicht-Python-Threads in der C-API heute funktionieren. Nach dem Aufruf von PyThreadState_Ensure() schließen Sie einfach den Interpreter-Guard, um dem Interpreter die Beendigung zu ermöglichen (und den aktuellen Thread für immer hängen zu lassen).

static int
thread_func(void *arg)
{
    PyInterpreterGuard guard = (PyInterpreterGuard)arg;
    PyThreadView thread_view = PyThreadState_Ensure(guard);
    if (thread_view == 0) {
        PyInterpreterGuard_Close(guard);
        return -1;
    }
    /* Close the interpreter guard, allowing it to
       finalize. This means that print(42) can hang this thread. */
    PyInterpreterGuard_Close(guard);
    if (PyRun_SimpleString("print(42)") < 0) {
        PyErr_Print();
    }
    PyThreadState_Release(thread_view);
    return 0;
}

static PyObject *
my_method(PyObject *self, PyObject *unused)
{
    PyThread_handle_t handle;
    PyThead_indent_t indent;

    PyInterpreterGuard guard = PyInterpreterGuard_FromCurrent();
    if (guard == 0) {
        return NULL;
    }

    if (PyThread_start_joinable_thread(thread_func, (void *)guard, &ident, &handle) < 0) {
        PyInterpreterGuard_Close(guard);
        return NULL;
    }
    Py_RETURN_NONE;
}

Beispiel: Ein asynchroner Callback

typedef struct {
    PyInterpreterView view;
} ThreadData;

static int
async_callback(void *arg)
{
    ThreadData *tdata = (ThreadData *)arg;
    PyInterpreterView view = tdata->view;
    PyInterpreterGuard guard = PyInterpreterGuard_FromView(view);
    if (guard == 0) {
        fputs("Python has shut down!\n", stderr);
        return -1;
    }

    PyThreadView thread_view = PyThreadState_Ensure(guard);
    if (thread_view == 0) {
        PyInterpreterGuard_Close(guard);
        return -1;
    }
    if (PyRun_SimpleString("print(42)") < 0) {
        PyErr_Print();
    }
    PyThreadState_Release(thread_view);
    PyInterpreterGuard_Close(guard);
    PyInterpreterView_Close(view);
    PyMem_RawFree(tdata);
    return 0;
}

static PyObject *
setup_callback(PyObject *self, PyObject *unused)
{
    // View to the interpreter. It won't wait on the callback
    // to finalize.
    ThreadData *tdata = PyMem_RawMalloc(sizeof(ThreadData));
    if (tdata == NULL) {
        PyErr_NoMemory();
        return NULL;
    }
    PyInterpreterView view = PyInterpreterView_FromCurrent();
    if (view == 0) {
        PyMem_RawFree(tdata);
        return NULL;
    }
    tdata->view = view;
    register_callback(async_callback, tdata);

    Py_RETURN_NONE;
}

Beispiel: Aufruf von Python ohne Callback-Parameter

Es gibt einige Fälle, in denen Callback-Funktionen keinen Callback-Parameter ( void *arg) annehmen, sodass es schwierig ist, einen Guard für einen bestimmten Interpreter zu erstellen. Die Lösung für dieses Problem besteht darin, einen Guard für den Hauptinterpreter über PyUnstable_InterpreterView_FromDefault() zu erstellen.

static void
call_python(void)
{
    PyInterpreterView view = PyUnstable_InterpreterView_FromDefault();
    if (guard == 0) {
        fputs("Python has shut down.", stderr);
        return;
    }

    PyInterpreterGuard guard = PyInterpreterGuard_FromView(view);
    if (guard == 0) {
        fputs("Python has shut down.", stderr);
        return;
    }

    PyThreadView thread_view = PyThreadState_Ensure(guard);
    if (thread_view == 0) {
        PyInterpreterGuard_Close(guard);
        PyInterpreterView_Close(view);
        return -1;
    }
    if (PyRun_SimpleString("print(42)") < 0) {
        PyErr_Print();
    }
    PyThreadState_Release(thread_view);
    PyInterpreterGuard_Close(guard);
    PyInterpreterView_Close(view);
    return 0;
}

Referenzimplementierung

Eine Referenzimplementierung dieses PEP finden Sie unter python/cpython#133110.

Offene Fragen

Wie sollen die APIs fehlschlagen?

Es gibt Meinungsverschiedenheiten darüber, wie die PyInterpreter[Guard|View]-APIs einen Fehler an den Aufrufer anzeigen sollen. Es gibt zwei konkurrierende Ideen:

  1. Gibt -1 zurück, um einen Fehler anzuzeigen, und 0 für Erfolg. Bei Erfolg werden Funktionen einem als Argument übergebenen PyInterpreter[Guard|View]-Zeiger zugewiesen.
  2. Gibt direkt einen PyInterpreter[Guard|View] zurück, wobei ein Wert von 0 NULL entspricht und einen Fehler anzeigt.

Derzeit spezifiziert das PEP letzteres.

Abgelehnte Ideen

Interpreter-Referenzzählung

Es gab zwei Iterationen dieses Vorschlags, die beide spezifizierten, dass ein Interpreter eine Referenzzählung beibehalten würde und auf das Erreichen dieses Zählers auf Null warten würde, bevor er herunterfährt.

Die erste Iteration dieser Idee geschah durch Hinzufügen impliziter Referenzzählung zu PyInterpreterState *-Zeigern. Eine Funktion namens PyInterpreterState_Hold würde die Referenzzählung erhöhen (wodurch sie zu einer "starken Referenz" wird), und PyInterpreterState_Release würde sie dekrementieren. Eine Interpreter-ID (ein eigenständiger int64_t) wurde als eine Form von schwacher Referenz verwendet, die verwendet werden konnte, um einen Interpreter-State nachzuschlagen und seine Referenzzählung atomar zu erhöhen. Diese Ideen wurden letztendlich abgelehnt, da sie die Dinge sehr verwirrend erscheinen ließen. Alle bestehenden Verwendungen von PyInterpreterState * wären ausgeliehen, was es für Entwickler schwierig machen würde zu verstehen, welche Teile ihres Codes eine starke Referenz erfordern oder verwenden.

Als Reaktion auf diese Gegenstimmen spezifizierte dieses PEP `PyInterpreterRef`-APIs, die ebenfalls die Referenzzählung nachahmen würden, jedoch auf explizitere Weise, was es für Entwickler einfacher machte. `PyInterpreterRef` war in diesem PEP analog zu PyInterpreterGuard. Ähnlich enthielt die ältere Überarbeitung `PyInterpreterWeakRef`, die analog zu PyInterpreterView war.

Schließlich wurde die Idee der Referenzzählung aus diesem Vorschlag aus mehreren Gründen vollständig verworfen

  1. Es gab Meinungsverschiedenheiten über die Überkomplizierung des API-Designs; das Referenzzählungsdesign sah HPy sehr ähnlich, was in CPython keine Präzedenzfälle hatte. Es gab die Befürchtung, dass dieser Vorschlag überkompliziert wurde, um mehr wie HPy auszusehen.
  2. Im Gegensatz zu herkömmlichen Referenzzählungs-APIs konnte das Erwerben einer starken Referenz auf einen Interpreter jederzeit fehlschlagen, und ein Interpreter würde nicht sofort freigegeben, wenn seine Referenzzahl Null erreichte.
  3. Es gab frühere Diskussionen über das Hinzufügen einer "echten" Referenzzählung zu Interpretern (die bei Erreichen von Null freigeben würde), was sehr verwirrend gewesen wäre, wenn es bereits eine bestehende API in CPython mit dem Titel `PyInterpreterRef` gäbe, die etwas anderes tut.

Nicht-Daemon-Thread-States

In früheren Überarbeitungen dieses PEP waren Interpreter-Guards eine Eigenschaft eines Thread-Zustands und keine Eigenschaft eines Interpreters. Dies bedeutete, dass `PyThreadState_Ensure()` einen Interpreter-Guard gehalten hat und dieser beim Aufruf von `PyThreadState_Release()` geschlossen wurde. Ein Thread-Zustand, der einen Guard für einen Interpreter hatte, wurde als "non-daemon thread state" bezeichnet. Zuerst schien dies eine Verbesserung zu sein, da es die Verwaltung der Lebensdauer eines Guards vom Thread anstatt vom Benutzer verlagerte, was etwas Boilerplate eliminierte.

Dies führte jedoch dazu, dass der Vorschlag erheblich komplexer wurde und die Ziele des Vorschlags beeinträchtigte

  • Am wichtigsten ist, dass Non-Daemon-Thread-Zustände zu viel Wert auf Daemon-Threads als Problem legen, was das PEP verwirrend machte. Darüber hinaus fügte die Formulierung "non-daemon" zusätzliche Verwirrung hinzu, da non-daemon Python-Threads explizit gejoint werden. Im Gegensatz dazu wird auf einen non-daemon C-Thread nur gewartet, bis er seinen Guard zerstört.
  • In vielen Fällen sollte ein Interpreter-Guard länger leben als ein einzelner Thread-Zustand. Das Stehlen des Interpreter-Guards in `PyThreadState_Ensure()` war in diesen Fällen besonders problematisch. Wenn `PyThreadState_Ensure()` keinen Guard mit Non-Daemon-Thread-Zuständen stehlen würde, würde dies die Eigentumsstory des Interpreter-Guards trüben und zu einem verwirrenderen API führen.

Exponieren einer Activate/Deactivate API anstelle von Ensure/Clear

In früheren Diskussionen dieses APIs wurde vorgeschlagen, tatsächliche `PyThreadState`-Zeiger in der API bereitzustellen, um den Besitz und die Lebensdauer des Thread-Zustands geradliniger zu gestalten

Wichtiger ist jedoch, dass dies klarer macht, wer den Thread-Zustand besitzt – ein manuell erstellter wird von dem Code kontrolliert, der ihn erstellt hat, und sobald er gelöscht ist, kann er nicht wieder aktiviert werden.

Dies wurde letztendlich aus zwei Gründen abgelehnt

  • Das vorgeschlagene API hat eine engere Verwendung mit `PyGILState_Ensure()` & `PyGILState_Release()`, was die Umstellung von alten Codebasen erleichtert.
  • Es ist signifikant einfacher für Code-Generatoren wie Cython zu verwenden, da keine zusätzliche Komplexität mit dem Verfolgen von `PyThreadState`-Zeigern verbunden ist.

Verwendung von PyStatus als Rückgabewert von PyThreadState_Ensure

In früheren Iterationen dieses APIs gab `PyThreadState_Ensure()` anstelle einer Ganzzahl einen `PyStatus` zurück, um Fehler anzuzeigen, was den Vorteil bot, eine Fehlermeldung bereitzustellen.

Dies wurde abgelehnt, da nicht klar ist, dass eine Fehlermeldung all das Nützliche wäre; alle konzipierten Anwendungsfälle für dieses API würden sich nicht wirklich um eine Nachricht kümmern, die angibt, warum Python nicht aufgerufen werden kann. Daher wäre das API nur unnötig komplexer zu verwenden, was wiederum die Umstellung von `PyGILState_Ensure()` beeinträchtigen würde.

Darüber hinaus wird `PyStatus` in der C-API nicht häufig verwendet. Einige Funktionen, die sich auf die Interpreter-Initialisierung beziehen, verwenden ihn (einfach, weil sie keine Ausnahmen auslösen können), und `PyThreadState_Ensure()` fällt nicht in diese Kategorie.

Danksagungen

Dieses PEP basiert auf früheren Arbeiten, Feedback und Diskussionen von vielen Personen, darunter Victor Stinner, Antoine Pitrou, David Woods, Sam Gross, Matt Page, Ronald Oussoren, Matt Wozniski, Eric Snow, Steve Dower, Petr Viktorin, Gregory P. Smith und Alyssa Coghlan.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0788.rst

Zuletzt geändert: 2025-10-14 11:22:45 GMT