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

Python Enhancement Proposals

PEP 311 – Vereinfachte Erfassung des Global Interpreter Lock für Erweiterungen

Autor:
Mark Hammond <mhammond at skippinet.com.au>
Status:
Final
Typ:
Standards Track
Erstellt:
05-Feb-2003
Python-Version:
2.3
Post-History:
05-Feb-2003, 14-Feb-2003, 19-Apr-2003

Inhaltsverzeichnis

Zusammenfassung

Dieses PEP schlägt eine vereinfachte API für den Zugriff auf den Global Interpreter Lock (GIL) für Python-Erweiterungsmodule vor. Insbesondere bietet es eine Lösung für Autoren komplexer Multi-Threaded-Erweiterungen, bei denen der aktuelle Zustand von Python (d.h. der Zustand des GIL) unbekannt ist.

Dieses PEP schlägt eine neue API für Plattformen vor, die mit Threading-Unterstützung kompiliert wurden, um den Python-Thread-Zustand zu verwalten. Eine Implementierungsstrategie wird vorgeschlagen, zusammen mit einer anfänglichen, plattformunabhängigen Implementierung.

Begründung

Die aktuelle API für den Python-Interpreter-Zustand ist für einfache, Single-Threaded-Erweiterungen geeignet, wird aber für nicht-triviale, Multi-Threaded-Erweiterungen schnell unglaublich komplex.

Derzeit bietet Python zwei Mechanismen für den Umgang mit dem GIL

  • Py_BEGIN_ALLOW_THREADS und Py_END_ALLOW_THREADS Makros. Diese Makros werden hauptsächlich bereitgestellt, um einer einfachen Python-Erweiterung, die bereits den GIL besitzt, zu ermöglichen, ihn vorübergehend freizugeben, während ein "externer" (d.h. nicht-Python), im Allgemeinen teurer, Aufruf getätigt wird. Alle vorhandenen Python-Threads, die auf den GIL warten, können dann ausgeführt werden. Dies ist zwar in Ordnung für Erweiterungen, die Aufrufe von Python in die Außenwelt tätigen, aber es hilft nicht für Erweiterungen, die Aufrufe in Python tätigen müssen, wenn der Thread-Zustand unbekannt ist.
  • PyThreadState und PyInterpreterState APIs. Diese API-Funktionen ermöglichen es einer Erweiterung/eingebetteten Anwendung, den GIL zu erfassen, leiden aber unter einem ernsten Bootstrapping-Problem – sie erfordern, dass Sie den Zustand des Python-Interpreters und des GIL kennen, bevor sie verwendet werden können. Ein besonderes Problem besteht für Erweiterungsautoren, die mit Threads umgehen müssen, die Python noch nie zuvor gesehen hat, aber Python von diesem Thread aus aufrufen müssen. Es ist sehr schwierig, heikel und fehleranfällig, eine Erweiterung zu schreiben, bei der diese "neuen" Threads immer den genauen Zustand des GIL kennen und daher zuverlässig mit dieser API interagieren können.

Aus diesen Gründen wird die Frage, wie solche Erweiterungen mit Python interagieren sollten, schnell zu einer FAQ. Der Hauptanreiz für dieses PEP, ein Thread auf python-dev [1], identifizierte sofort die folgenden Projekte mit diesem genauen Problem

  • Die win32all-Erweiterungen
  • Boost
  • ctypes
  • Python-GTK-Bindings
  • Uno
  • PyObjC
  • Mac Toolbox
  • PyXPCOM

Derzeit gibt es keine sinnvolle, portable Lösung für dieses Problem, die jeden Erweiterungsautor zwingt, seine eigene selbst implementierte Version zu erstellen. Darüber hinaus ist das Problem komplex, was bedeutet, dass viele Implementierungen wahrscheinlich falsch sein werden, was zu einer Vielzahl von Problemen führt, die sich oft einfach als "Python ist eingefroren" äußern.

Obwohl das größte Problem der bestehenden Thread-State-API die fehlende Möglichkeit ist, den aktuellen Zustand des Locks abzufragen, wird der Meinung nach eine vollständigere, vereinfachte Lösung für Erweiterungsautoren angeboten werden. Eine solche Lösung sollte Autoren ermutigen, fehlerfreie, komplexe Erweiterungsmodule bereitzustellen, die die Threading-Mechanismen von Python voll ausnutzen.

Einschränkungen und Ausschlüsse

Dieser Vorschlag identifiziert eine Lösung für Erweiterungsautoren mit komplexen Multi-Threaded-Anforderungen, die jedoch nur einen einzigen "PyInterpreterState" benötigen. Es wird kein Versuch unternommen, Erweiterungen zu unterstützen, die mehrere Interpreter-Zustände benötigen. Zum Zeitpunkt der Erstellung wurde keine Erweiterung identifiziert, die mehrere PyInterpreterStates benötigt, und es ist tatsächlich unklar, ob diese Einrichtung in Python selbst korrekt funktioniert.

Diese API führt keine automatische Initialisierung von Python durch und initialisiert Python nicht für den Multi-Threaded-Betrieb. Erweiterungsautoren müssen weiterhin Py_Initialize() aufrufen, und für Multi-Threaded-Anwendungen PyEval_InitThreads(). Der Grund dafür ist, dass der erste Thread, der PyEval_InitThreads() aufruft, von Python als "Hauptthread" nominiert wird. Daher entfernt die Anforderung an den Erweiterungsautor, den Hauptthread anzugeben (indem dieser erste Aufruf gemacht wird), Mehrdeutigkeiten. Da Py_Initialize() vor PyEval_InitThreads() aufgerufen werden muss und beide Funktionen derzeit mehrmalige Aufrufe unterstützen, wird die Belastung für Erweiterungsautoren als angemessen erachtet.

Diese API soll alles sein, was zur Erfassung des Python-GIL notwendig ist. Abgesehen von den bestehenden Standard-Makros Py_BEGIN_ALLOW_THREADS und Py_END_ALLOW_THREADS wird davon ausgegangen, dass keine zusätzlichen Thread-State-API-Funktionen von der Erweiterung verwendet werden. Erweiterungen mit solch komplizierten Anforderungen können weiterhin die bestehende Thread-State-API verwenden.

Vorschlag

Dieser Vorschlag empfiehlt die Hinzufügung einer neuen API zu Python, um die Verwaltung des GIL zu vereinfachen. Diese API wird auf allen Plattformen verfügbar sein, die mit WITH_THREAD definiert kompiliert wurden.

Die Absicht ist, dass ein Erweiterungsautor, unter der Annahme, dass Python korrekt initialisiert wurde, jederzeit und auf jedem Thread einen kleinen, gut definierten "Prologtanz" durchführen kann, der sicherstellt, dass Python auf diesem Thread einsatzbereit ist. Nachdem die Erweiterung mit Python fertig ist, muss sie auch einen "Epilogtanz" durchführen, um alle zuvor erfassten Ressourcen freizugeben. Idealerweise können diese Tänze in einer einzigen Zeile ausgedrückt werden.

Insbesondere werden die folgenden neuen APIs vorgeschlagen

/* Ensure that the current thread is ready to call the Python
   C API, regardless of the current state of Python, or of its
   thread lock.  This may be called as many times as desired
   by a thread so long as each call is matched with a call to
   PyGILState_Release().  In general, other thread-state APIs may
   be used between _Ensure() and _Release() calls, so long as the
   thread-state is restored to its previous state before the Release().
   For example, normal use of the Py_BEGIN_ALLOW_THREADS/
   Py_END_ALLOW_THREADS macros are acceptable.

   The return value is an opaque "handle" to the thread state when
   PyGILState_Acquire() was called, and must be passed to
   PyGILState_Release() to ensure Python is left in the same state. Even
   though recursive calls are allowed, these handles can *not* be
   shared - each unique call to PyGILState_Ensure must save the handle
   for its call to PyGILState_Release.

   When the function returns, the current thread will hold the GIL.

   Failure is a fatal error.
*/
PyAPI_FUNC(PyGILState_STATE) PyGILState_Ensure(void);

/* Release any resources previously acquired.  After this call, Python's
   state will be the same as it was prior to the corresponding
   PyGILState_Acquire call (but generally this state will be unknown to
   the caller, hence the use of the GILState API.)

   Every call to PyGILState_Ensure must be matched by a call to
   PyGILState_Release on the same thread.
*/
PyAPI_FUNC(void) PyGILState_Release(PyGILState_STATE);

Die allgemeine Verwendung wird sein

void SomeCFunction(void)
{
    /* ensure we hold the lock */
    PyGILState_STATE state = PyGILState_Ensure();
    /* Use the Python API */
    ...
    /* Restore the state of Python */
    PyGILState_Release(state);
}

Design und Implementierung

Der allgemeine Betrieb von PyGILState_Ensure() wird sein

  • Assertion, dass Python initialisiert ist.
  • Holen Sie sich einen PyThreadState für den aktuellen Thread, erstellen und speichern Sie ihn, falls erforderlich.
  • Merken Sie sich den aktuellen Zustand des Locks (besessen/nicht besessen)
  • Wenn der aktuelle Zustand den GIL nicht besitzt, erfassen Sie ihn.
  • Inkrementieren Sie einen Zähler für die Anzahl der Aufrufe von PyGILState_Ensure auf dem aktuellen Thread.
  • zurückkehren

Der allgemeine Betrieb von PyGILState_Release() wird sein

  • Assertion, dass unser Thread derzeit den Lock hält.
  • Wenn der alte Zustand anzeigt, dass der Lock zuvor nicht besessen wurde, geben Sie den GIL frei.
  • Dekrementieren Sie den PyGILState_Ensure Zähler für den Thread.
  • Wenn Zähler == 0
    • Geben Sie den PyThreadState frei und löschen Sie ihn.
    • Vergessen Sie, dass der ThreadState vom Thread besessen wird.
  • zurückkehren

Es wird davon ausgegangen, dass es ein Fehler ist, wenn zwei diskrete PyThreadStates für einen einzelnen Thread verwendet werden. Kommentare in pystate.h ("State unique per thread") unterstützen diese Ansicht, obwohl sie nie direkt angegeben ist. Dies erfordert daher eine Implementierung von Thread Local Storage. Glücklicherweise existiert eine plattformunabhängige Implementierung von Thread Local Storage bereits im Quellbaum von Python, im SGI-Threading-Port. Dieser Code wird in den plattformunabhängigen Python-Kern integriert, aber so, dass Plattformen bei Bedarf eine optimiertere Implementierung bereitstellen können.

Implementierung

Eine Implementierung dieses Vorschlags finden Sie unter https://bugs.python.org/issue684256

Referenzen


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

Zuletzt geändert: 2025-02-01 08:59:27 GMT