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

Python Enhancement Proposals

PEP 684 – Ein GIL pro Interpreter

Autor:
Eric Snow <ericsnowcurrently at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Benötigt:
683
Erstellt:
08. März 2022
Python-Version:
3.12
Post-History:
08. März 2022, 29. Sep 2022, 28. Okt 2022
Resolution:
Discourse-Nachricht

Inhaltsverzeichnis

Zusammenfassung

Seit Python 1.5 (1997) können CPython-Benutzer mehrere Interpreter im selben Prozess ausführen. Interpreter im selben Prozess haben jedoch immer einen erheblichen Teil ihres globalen Status gemeinsam genutzt. Dies ist eine Quelle von Fehlern mit wachsender Auswirkung, da immer mehr Menschen die Funktion nutzen. Darüber hinaus würde eine ausreichende Isolierung eine echte Multi-Core-Parallelität ermöglichen, bei der sich Interpreter nicht mehr das GIL teilen. Die in diesem Vorschlag dargelegten Änderungen werden zu diesem Grad der Interpreter-Isolation führen.

Zusammenfassung auf hoher Ebene

Auf hoher Ebene ändert dieser Vorschlag CPython auf folgende Weise:

  • teilt das GIL nicht mehr zwischen Interpretern, bei ausreichender Isolierung
  • fügt mehrere neue Interpreter-Konfigurationsoptionen für Isolationseinstellungen hinzu
  • verhindert, dass inkompatible Erweiterungen Probleme verursachen

Das GIL

Das GIL schützt den gleichzeitigen Zugriff auf den größten Teil des CPython-Laufzeitstatus. Daher muss der gesamte GIL-geschützte globale Status in jeden Interpreter verschoben werden, bevor das GIL verschoben werden kann.

(In einer Handvoll Fällen können andere Mechanismen verwendet werden, um eine threadsichere gemeinsame Nutzung zu gewährleisten, wie z. B. Sperren oder "unsterbliche" Objekte.)

CPython Laufzeitstatus

Die ordnungsgemäße Isolierung von Interpretern erfordert, dass der größte Teil des CPython-Laufzeitstatus in der PyInterpreterState-Struktur gespeichert wird. Derzeit ist nur ein Teil davon dort gespeichert; der Rest befindet sich entweder in C-Globalvariablen oder in _PyRuntimeState. Ein Großteil davon muss verschoben werden.

Dies fällt mit einer laufenden Anstrengung (seit vielen Jahren) zusammen, die interne Verwendung von globalen Variablen stark zu reduzieren und den Laufzeitstatus in _PyRuntimeState und PyInterpreterState zu konsolidieren. (Siehe Konsolidierung des globalen Laufzeitstatus unten.) Dieses Projekt hat alleine erhebliche Vorteile und hat wenig Kontroversen hervorgerufen. Während also ein GIL pro Interpreter die Fertigstellung dieser Anstrengung voraussetzt, sollte dieses Projekt nicht als Teil dieses Vorschlags betrachtet werden – nur als Abhängigkeit.

Weitere Isolationsüberlegungen

CPython-Interpreter müssen streng voneinander isoliert sein, mit wenigen Ausnahmen. Bis zu einem gewissen Grad sind sie es bereits. Jeder Interpreter hat seine eigene Kopie aller Module, Klassen, Funktionen und Variablen. Die CPython C-API-Dokumentation erläutert dies weiter.

Abgesehen von dem, was bereits erwähnt wurde (z. B. das GIL), gibt es jedoch ein paar Möglichkeiten, wie Interpreter immer noch einen Teil des Status gemeinsam nutzen.

Erstens werden einige prozessglobale Ressourcen (z. B. Speicher, Dateideskriptoren, Umgebungsvariablen) gemeinsam genutzt. Es gibt keine Pläne, dies zu ändern.

Zweitens ist eine gewisse Isolierung aufgrund von Fehlern oder Implementierungen, die keine mehreren Interpreter berücksichtigten, fehlerhaft. Dies umfasst die Laufzeit und die Standardbibliothek von CPython sowie Erweiterungsmodule, die auf globalen Variablen basieren. In solchen Fällen sollten Fehler gemeldet werden, da einige bereits gemeldet wurden.

Abhängigkeit von "Immortal Objects"

PEP 683 führt "immortal objects" als interne CPython-Funktion ein. Mit "immortal objects" können wir alle ansonsten unveränderlichen globalen Objekte zwischen allen Interpretern gemeinsam nutzen. Folglich muss sich dieser PEP nicht damit befassen, wie mit den verschiedenen Objekten umzugehen ist, die in der öffentlichen C-API verfügbar sind. Er vereinfacht auch die Frage, was mit den eingebauten statischen Typen geschehen soll. (Siehe Globale Objekte unten.)

Beide Probleme haben alternative Lösungen, aber mit "immortal objects" ist alles einfacher. Wenn PEP 683 nicht akzeptiert wird, wird dieser PEP mit den Alternativen aktualisiert. Dies ermöglicht es uns, das Rauschen in diesem Vorschlag zu reduzieren.

Motivation

Das grundlegende Problem, das wir hier lösen, ist der Mangel an echter Multi-Core-Parallelität (für Python-Code) in der CPython-Laufzeit. Das GIL ist die Ursache. Obwohl es in der Praxis normalerweise kein Problem darstellt, macht es die Multi-Core-Geschichte von Python zumindest trüb, was das GIL zu einer ständigen Ablenkung macht.

Isolierte Interpreter sind auch ein effektiver Mechanismus zur Unterstützung bestimmter Nebenläufigkeitsmodelle. PEP 554 diskutiert dies im Detail.

Indirekte Vorteile

Die meiste Arbeit, die für ein GIL pro Interpreter erforderlich ist, hat Vorteile, die diese Aufgaben ohnehin lohnenswert machen

  • macht das Verhalten mehrerer Interpreter zuverlässiger
  • hat zu Korrekturen langjähriger Laufzeitfehler geführt, die sonst keine Priorität hatten
  • hat bisher unbekannte Laufzeitfehler aufgedeckt (und Korrekturen dafür inspiriert)
  • hat eine sauberere Laufzeitinitialisierung vorangetrieben (PEP 432, PEP 587)
  • hat eine sauberere und vollständigere Laufzeitfinalisierung vorangetrieben
  • hat zu einer strukturellen Schichtung der C-API geführt (z. B. Include/internal)
  • siehe auch Vorteile der Konsolidierung unten

Darüber hinaus profitiert viel von dieser Arbeit anderen CPython-bezogenen Projekten

Bestehende Nutzung mehrerer Interpreter

Die C-API für mehrere Interpreter wird seit vielen Jahren verwendet. Bis vor relativ kurzer Zeit war die Funktion jedoch weder weit verbreitet noch extensiv genutzt (mit Ausnahme von mod_wsgi).

In den letzten Jahren hat die Nutzung mehrerer Interpreter zugenommen. Hier sind einige der öffentlichen Projekte, die die Funktion derzeit nutzen:

Beachten Sie, dass mit PEP 554 die Nutzung mehrerer Interpreter wahrscheinlich erheblich zunehmen würde (über Python-Code und nicht über die C-API).

PEP 554 (Mehrere Interpreter in der Standardbibliothek)

PEP 554 beschäftigt sich ausschließlich damit, ein minimales Standardbibliotheksmodul bereitzustellen, um Benutzern den Zugriff auf mehrere Interpreter aus Python-Code zu ermöglichen. Tatsächlich vermeidet es ausdrücklich, Änderungen im Zusammenhang mit dem GIL vorzuschlagen. Bedenken Sie jedoch, dass Benutzer dieses Moduls von einem GIL pro Interpreter profitieren würden, was PEP 554 attraktiver macht.

Begründung

Während der ersten Untersuchungen im Jahr 2014 wurden verschiedene mögliche Lösungen für Multi-Core-Python untersucht, aber jede hatte Nachteile ohne einfache Lösungen:

  • die bestehende Praxis, das GIL in Erweiterungsmodulen freizugeben
    • hilft nicht bei Python-Code
  • andere Python-Implementierungen (z. B. Jython, IronPython)
    • CPython dominiert die Community
  • entfernen Sie das GIL (z. B. "gilectomy", "no-gil")
    • zu hohes technisches Risiko (zu dieser Zeit)
  • Trent Nelsons "PyParallel"-Projekt
    • unvollständig; zu dieser Zeit nur für Windows
  • multiprocessing
    • zu viel Arbeit, um es effektiv genug zu machen; hohe Strafen in einigen Situationen (in großem Maßstab, Windows)
  • andere Parallelitätstools (z. B. dask, ray, MPI)
    • keine passende Lösung für Laufzeit/Standardbibliothek
  • geben Sie Multi-Core auf (z. B. async, nichts tun)
    • das kann nur in Tränen enden

Schon 2014 war ziemlich klar, dass eine Lösung mit isolierten Interpretern kein hohes technisches Risiko hatte und dass die meiste Arbeit ohnehin lohnenswert war. (Der Nachteil war der Umfang der zu erledigenden Arbeit.)

Spezifikation

Wie oben zusammengefasst, beinhaltet dieser Vorschlag die folgenden Änderungen, in der Reihenfolge, in der sie erfolgen müssen:

  1. konsolidieren Sie den globalen Laufzeitstatus (einschließlich Objekte) in _PyRuntimeState
  2. verschieben Sie fast den gesamten Status nach unten in PyInterpreterState
  3. verschieben Sie schließlich das GIL nach unten in PyInterpreterState
  4. alles andere
    • aktualisieren Sie die C-API
    • implementieren Sie Einschränkungen für Erweiterungsmodule
    • arbeiten Sie mit beliebten Erweiterungen zusammen, um sie bei der Unterstützung mehrerer Interpreter zu unterstützen

Status pro Interpreter

Der folgende Laufzeitstatus wird in PyInterpreterState verschoben:

  • alle globalen Objekte, die nicht sicher gemeinsam genutzt werden können (vollständig unveränderlich)
  • das GIL
  • die meisten veränderlichen Daten, die derzeit vom GIL geschützt werden
  • veränderliche Daten, die derzeit durch eine andere Interpreter-spezifische Sperre geschützt sind
  • veränderliche Daten, die unabhängig in verschiedenen Interpretern verwendet werden können (gilt auch für Erweiterungsmodule, einschließlich solcher mit Multi-Phase-Initialisierung)
  • alle anderen veränderlichen Daten, die nicht unten anderweitig ausgeschlossen sind

Darüber hinaus wurde ein Teil des vollständigen globalen Status bereits in den Interpreter verschoben, einschließlich GC, Warnungen und Atexit-Hooks.

Der folgende Laufzeitstatus wird nicht verschoben:

  • globale Objekte, die sicher gemeinsam genutzt werden können, falls vorhanden
  • unveränderliche Daten, oft const
  • effektiv unveränderliche Daten (als unveränderlich behandelt), zum Beispiel
    • einige Status werden frühzeitig initialisiert und danach nie wieder geändert
    • Hashes für Zeichenfolgen (PyUnicodeObject) werden beim ersten Bedarf idempotent berechnet und dann zwischengespeichert
  • alle Daten, die garantiert ausschließlich im Hauptthread modifiziert werden, einschließlich
    • Status, der nur im CPython-main() verwendet wird
    • der Status der REPL
    • Daten, die nur während der Laufzeitinitialisierung modifiziert werden (danach effektiv unveränderlich)
  • veränderliche Daten, die durch eine globale Sperre (außer dem GIL) geschützt sind
  • globaler Status in atomaren Variablen
  • veränderlicher globaler Status, der (sinnvoll) in atomare Variablen geändert werden kann

Speicherallokatoren

Dies ist einer der heikelsten Teile der Arbeit zur Isolierung von Interpretern. Die einfachste Lösung ist, den globalen Status des internen "Small-Block"-Allokators nach PyInterpreterState zu verschieben, wie wir es mit fast allen anderen Laufzeitstatus tun. Das Folgende erläutert die Details und die Begründung.

CPython bietet eine C-API für die Speicherverwaltung mit drei Allokator-Domänen: "raw", "mem" und "object". Jede bietet das Äquivalent zu malloc(), calloc(), realloc() und free(). Ein benutzerdefinierter Allokator für jede Domäne kann während der Laufzeitinitialisierung gesetzt werden, und der aktuelle Allokator kann mit einem Hook über dieselbe API umwickelt werden (z. B. das Standardbibliotheksmodul tracemalloc). Die Allokatoren sind derzeit Laufzeit-global und werden von allen Interpretern gemeinsam genutzt.

Es wird erwartet, dass der "raw"-Allokator threadsicher ist und standardmäßig glibc's Allokator (malloc() usw.) verwendet. Die "mem"- und "object"-Allokatoren werden jedoch nicht als threadsicher erwartet und können derzeit für die Thread-Sicherheit auf das GIL angewiesen sein. Dies liegt zum Teil daran, dass der Standardallokator für beide, auch als "pyobject" bekannt, nicht threadsicher ist. Dies liegt daran, wie der gesamte Status für diesen Allokator in C-Globalvariablen gespeichert ist. (Siehe Objects/obmalloc.c.)

Somit kommen wir zurück zur Frage der Isolierung des Laufzeitstatus. Damit Interpreter aufhören, das GIL gemeinsam zu nutzen, muss die Threadsicherheit der Allokatoren behandelt werden. Wenn Interpreter die Allokatoren weiterhin gemeinsam nutzen, benötigen wir eine andere Methode zur Gewährleistung der Thread-Sicherheit. Andernfalls müssen Interpreter aufhören, die Allokatoren gemeinsam zu nutzen. In beiden Fällen gibt es eine Reihe möglicher Lösungen, jede mit potenziellen Nachteilen.

Um die Allokatoren weiterhin gemeinsam zu nutzen, ist die einfachste Lösung, eine granulare Laufzeit-globale Sperre um die Aufrufe der "mem"- und "object"-Allokatoren in PyMem_Malloc(), PyObject_Malloc() usw. zu verwenden. Dies würde die Leistung beeinträchtigen, aber es gibt einige Möglichkeiten, dies abzumildern (z. B. nur dann mit der Sperrung beginnen, wenn der erste Sub-Interpreter erstellt wurde).

Eine andere Möglichkeit, die Allokatoren gemeinsam zu nutzen, besteht darin, zu verlangen, dass die "mem"- und "object"-Allokatoren threadsicher sind. Das würde bedeuten, dass wir den pyobject-Allokator-Implementierung threadsicher machen müssten. Dies könnte sogar die Neuimplementierung mit einem erweiterbaren Allokator wie mimalloc beinhalten. Der potenzielle Nachteil liegt in den Kosten der Neuimplementierung des Allokators und dem Risiko von Fehlern, die mit einem solchen Unterfangen verbunden sind.

Unabhängig davon würde eine Umstellung auf die Anforderung von threadsicheren Allokatoren jeden beeinträchtigen, der CPython einbettet und derzeit einen nicht threadsicheren Allokator einstellt. Wir müssten überlegen, wer betroffen sein könnte und wie wir negative Auswirkungen reduzieren können (z. B. eine einfache C-API hinzufügen, um einen Allokator threadsicher zu machen).

Wenn wir die Allokatoren zwischen Interpretern nicht mehr gemeinsam nutzen würden, müssten wir dies nur für die "mem"- und "object"-Allokatoren tun. Möglicherweise müssen wir auch einen vollständigen Satz globaler Allokatoren für bestimmte Laufzeitverwendungen beibehalten. Es gäbe eine gewisse Leistungseinbuße durch das Nachschlagen des aktuellen Interpreters und anschließende Zeigerindirektion, um die Allokatoren zu erhalten. Embedder müssten wahrscheinlich auch einen neuen Allokator-Kontext für jeden Interpreter bereitstellen. Positiv ist, dass Allokator-Hooks (z. B. tracemalloc) nicht betroffen wären.

Letztendlich werden wir uns für die einfachste Option entscheiden:

  • behalten Sie die Allokatoren im globalen Laufzeitstatus
  • verlangen Sie, dass sie threadsicher sind
  • verschieben Sie den Status des Standard-Objektallokators (auch bekannt als "Small-Block"-Allokator) nach PyInterpreterState

Wir haben mit einer groben Implementierung experimentiert und festgestellt, dass sie ziemlich unkompliziert war und die Leistungseinbuße praktisch null betrug.

C-API

Intern verfolgt der Interpreter-Status nun, wie das Importsystem mit Erweiterungsmodulen umgehen soll, die nicht für die Verwendung mit mehreren Interpretern unterstützt werden. Siehe Beschränkung von Erweiterungsmodulen unten. Wir werden diese Einstellung hier als "PyInterpreterState.strict_extension_compat" bezeichnen.

Die folgende API wird öffentlich gemacht, falls sie es noch nicht ist:

  • PyInterpreterConfig (Struktur)
  • PyInterpreterConfig_INIT (Makro)
  • PyInterpreterConfig_LEGACY_INIT (Makro)
  • PyThreadState * Py_NewInterpreterFromConfig(PyInterpreterConfig *)

Wir werden zwei neue Felder zu PyInterpreterConfig hinzufügen:

  • int own_gil
  • int strict_extensions_compat

Wir können im Laufe der Zeit weitere Felder hinzufügen, falls erforderlich (z. B. "own_initial_thread").

Bezüglich der Initialisierer-Makros würde PyInterpreterConfig_INIT verwendet, um einen isolierten Interpreter zu erhalten, der auch subinterpreter-unfreundliche Funktionen vermeidet. Dies wäre der Standard für über PEP 554 erstellte Interpreter. Die uneingeschränkte (Status quo) bleibt über PyInterpreterConfig_LEGACY_INIT verfügbar, die bereits für den Hauptinterpreter und Py_NewInterpreter() verwendet wird. Dies wird sich nicht ändern.

Eine Anmerkung zum "Haupt"-Interpreter

Unten erwähnen wir mehrmals den "Haupt"-Interpreter. Dies bezieht sich auf den Interpreter, der während der Laufzeitinitialisierung erstellt wird und dessen anfänglicher PyThreadState dem Hauptthread des Prozesses entspricht. Er hat eine Reihe einzigartiger Verantwortlichkeiten (z. B. die Behandlung von Signalen) sowie eine besondere Rolle während der Laufzeitinitialisierung/-finalisierung. Er ist auch normalerweise (für den Moment) der einzige Interpreter. (Siehe auch https://docs.pythonlang.de/3/c-api/init.html#sub-interpreter-support.)

PyInterpreterConfig.own_gil

Wenn true (1), hat der neue Interpreter sein eigenes "globales" Interpreter-Schloss. Das bedeutet, dass der neue Interpreter laufen kann, ohne von anderen Interpretern unterbrochen zu werden. Dies ermöglicht effektiv die volle Nutzung mehrerer Kerne. Das ist das grundlegende Ziel dieses PEP.

Wenn false (0), verwendet der neue Interpreter die Sperre des Hauptinterpreters. Dies ist das Legacy-Verhalten (vor 3.12) in CPython, bei dem alle Interpreter ein einzelnes GIL gemeinsam nutzen. Das gemeinsame Nutzen des GIL kann wünschenswert sein, wenn Erweiterungsmodule verwendet werden, die für die Threadsicherheit immer noch auf das GIL angewiesen sind.

In PyInterpreterConfig_INIT ist dies true. In PyInterpreterConfig_LEGACY_INIT ist dies false.

Außerdem werden wir zur Sicherheit vorerst own_gil nicht auf true setzen, wenn während der Laufzeitinitialisierung ein benutzerdefinierter Allokator gesetzt wurde. Das Umwickeln des Allokators, wie bei tracemalloc, ist weiterhin in Ordnung.

PyInterpreterConfig.strict_extensions_compat

PyInterpreterConfig.strict_extensions_compat ist im Grunde der Anfangswert für "PyInterpreterState.strict_extension_compat".

Beschränkung von Erweiterungsmodulen

Erweiterungsmodule haben viele der gleichen Probleme wie die Laufzeit, wenn der Status in globalen Variablen gespeichert ist. PEP 630 behandelt alle Details, die Erweiterungen unterstützen müssen, um die Isolierung zu unterstützen und somit sicher in mehreren Interpretern gleichzeitig zu laufen. Dies beinhaltet den Umgang mit ihren globalen Variablen.

Wenn eine Erweiterung eine mehrstufige Initialisierung implementiert (siehe PEP 489), gilt sie als mit mehreren Interpretern kompatibel. Alle anderen Erweiterungen gelten als inkompatibel. (Siehe Thread-Sicherheit von Erweiterungsmodulen für weitere Details, wie sich ein GIL pro Interpreter auf diese Klassifizierung auswirken kann.)

Wenn eine inkompatible Erweiterung importiert wird und der aktuelle Wert von "PyInterpreterState.strict_extension_compat" true ist, löst das Importsystem einen ImportError aus. (Bei false wird einfach nicht geprüft.) Dies geschieht über importlib._bootstrap_external.ExtensionFileLoader (genauer gesagt über _imp.create_dynamic(), _PyImport_LoadDynamicModuleWithSpec() und PyModule_FromDefAndSpec2()).

Solche Importe werden im Hauptinterpreter (oder in über Py_NewInterpreter() erstellten Interpretern) niemals fehlschlagen, da "PyInterpreterState.strict_extension_compat" in beiden Fällen mit false initialisiert wird. Somit bleibt das Legacy-Verhalten (vor 3.12) erhalten.

Wir werden mit beliebten Erweiterungen zusammenarbeiten, um ihnen bei der Unterstützung der Verwendung in mehreren Interpretern zu helfen. Dies kann die Erweiterung der öffentlichen C-API von CPython beinhalten, die wir von Fall zu Fall behandeln werden.

Kompatibilität von Erweiterungsmodulen

Wie in Erweiterungsmodule erwähnt, funktionieren viele Erweiterungen auch in mehreren Interpretern (und unter einem GIL pro Interpreter) ohne Änderungen. Das Importsystem schlägt immer noch fehl, wenn ein solches Modul die Unterstützung nicht explizit anzeigt. Zunächst werden nicht viele Erweiterungsmodule dies tun, was zu Frustration führen kann.

Wir werden dies beheben, indem wir einen Kontextmanager hinzufügen, um die Prüfung auf Unterstützung für mehrere Interpreter vorübergehend zu deaktivieren: importlib.util.allow_all_extensions(). Er wird mehr oder weniger den aktuellen Wert von "PyInterpreterState.strict_extension_compat" modifizieren (z. B. über eine private sys-Funktion).

Thread-Sicherheit von Erweiterungsmodulen

Wenn ein Modul die Verwendung mit mehreren Interpretern unterstützt, bedeutet dies größtenteils, dass es auch dann funktioniert, wenn diese Interpreter das GIL nicht gemeinsam nutzen. Die einzige Ausnahme ist, wenn ein Modul gegen eine Bibliothek mit internem globalem Status verknüpft ist, der nicht threadsicher ist. (Selbst etwas so Harmloses wie eine statische lokale Variable als temporärer Puffer kann ein Problem sein.) Mit einem gemeinsamen GIL ist dieser Status geschützt. Ohne einen müssen solche Module jede Verwendung dieses Status (z. B. durch Aufrufe) mit einer Sperre umschließen.

Derzeit ist nicht klar, ob "supports-multiple-interpreters" ausreichend äquivalent zu "supports-per-interpreter-gil" ist, sodass wir auf besondere Vorkehrungen verzichten können. Dies ist immer noch ein Punkt der sinnvollen Diskussion und Untersuchung. Die praktische Unterscheidung zwischen den beiden (in der Python-Community, z. B. PyPI) ist noch nicht gut genug verstanden, um die Angelegenheit zu klären. Ebenso ist nicht klar, was wir tun könnten, um die Pfleger von Erweiterungen bei der Minderung des Problems zu unterstützen (vorausgesetzt, es ist eines).

In der Zwischenzeit müssen wir so vorgehen, als ob der Unterschied groß genug wäre, um Probleme für genügend bestehende Erweiterungsmodule zu verursachen. Die Lösung, die wir anwenden würden, ist:

  • fügen Sie einen PyModuleDef-Slot hinzu, der angibt, dass eine Erweiterung unter einem GIL pro Interpreter importiert werden kann (d. h. Opt-in)
  • fügen Sie diesen Slot als Teil der Definition einer "kompatiblen" Erweiterung hinzu, wie oben besprochen

Der Nachteil ist, dass nicht ein einziges Erweiterungsmodul das GIL pro Interpreter nutzen kann, ohne zusätzliche Anstrengungen des Modulpflegers, egal wie gering diese Anstrengung ist. Dies verschärft das Problem, das in Kompatibilität von Erweiterungsmodulen beschrieben wird, und die gleiche Problemumgehung gilt. Idealerweise würden wir feststellen, dass es keinen ausreichenden Unterschied gibt, der eine Rolle spielt.

Wenn wir letztendlich ein Opt-in für Importe unter einem GIL pro Interpreter erzwingen und später feststellen, dass es nicht notwendig ist, können wir den Standard zu diesem Zeitpunkt ändern, den alten Opt-in-Slot zu einer No-Operation machen und einen neuen PyModuleDef-Slot für explizites Opt-out hinzufügen. Tatsächlich ist es sinnvoll, diesen Opt-out-Slot von Anfang an hinzuzufügen.

Dokumentation

  • C-API: Der Abschnitt "Sub-Interpreter-Unterstützung" von Doc/c-api/init.rst wird die aktualisierte API detailliert beschreiben
  • C-API: Dieser Abschnitt wird die Konsequenzen eines GIL pro Interpreter erläutern
  • importlib: Der Eintrag ExtensionFileLoader wird darauf hinweisen, dass Importe in Sub-Interpretern fehlschlagen können
  • importlib: Es wird einen neuen Eintrag über importlib.util.allow_all_extensions() geben

Auswirkungen

Abwärtskompatibilität

Durch diesen Vorschlag sollen sich keine Verhaltensweisen oder APIs ändern, mit zwei Ausnahmen:

  • einige Erweiterungen schlagen fehl, in einigen Sub-Interpretern importiert zu werden (siehe den nächsten Abschnitt)
  • "mem"- und "object"-Allokatoren, die derzeit nicht threadsicher sind, können nun bei Verwendung in Kombination mit mehreren Interpretern anfällig für Datenrennen sein

Die bestehende C-API zur Verwaltung von Interpretern behält ihr aktuelles Verhalten bei, wobei neues Verhalten über neue APIs verfügbar gemacht wird. Keine andere API oder kein Laufzeitverhalten soll sich ändern, einschließlich der Kompatibilität mit der stabilen ABI.

Siehe Objekte, die in der C-API verfügbar sind unten für verwandte Diskussionen.

Erweiterungsmodule

Derzeit ist die bei weitem häufigste Nutzung von Python mit dem Hauptinterpreter, der allein läuft. Dieser Vorschlag hat keine Auswirkungen auf Erweiterungsmodule in diesem Szenario. Ebenso gibt es, ob gut oder schlecht, keine Verhaltensänderung unter mehreren über die bestehende Py_NewInterpreter() erstellten Interpretern.

Beachten Sie, dass einige Erweiterungen bereits brechen, wenn sie in mehreren Interpretern verwendet werden, da sie den Modulstatus in globalen Variablen beibehalten (oder aufgrund des internen Status von verknüpften Bibliotheken). Sie können abstürzen oder, schlimmer noch, inkonsistentes Verhalten zeigen. Das war Teil der Motivation für PEP 630 und ähnliche PEPs, also ist dies keine neue Situation und auch keine Folge dieses Vorschlags.

Im Gegensatz dazu ändert sich bei Verwendung der vorgeschlagenen API zur Erstellung mehrerer Interpreter mit den entsprechenden Einstellungen das Verhalten für inkompatible Erweiterungen. In diesem Fall schlägt der Import einer solchen Erweiterung fehl (außerhalb des Hauptinterpreters), wie in Beschränkung von Erweiterungsmodulen erklärt. Für Erweiterungen, die bereits in mehreren Interpretern fehlschlagen, wird dies eine Verbesserung sein.

Zusätzlich verknüpfen sich einige Erweiterungsmodule mit Bibliotheken, die nicht threadsichere interne globale Zustände haben. (Siehe Thread-Sicherheit von Erweiterungsmodulen.) Solche Module müssen jeden direkten oder indirekten Zugriff auf diesen Status mit einer Sperre umschließen. Dies ist der Hauptunterschied zu anderen Modulen, die ebenfalls eine mehrstufige Initialisierung implementieren und somit die Unterstützung für mehrere Interpreter (d. h. Isolierung) anzeigen.

Nun kommen wir zu dem oben erwähnten Bruch der Kompatibilität. Einige Erweiterungen sind sicher für mehrere Interpreter (und ein GIL pro Interpreter), obwohl sie dies nicht angezeigt haben. Leider gibt es keine zuverlässige Möglichkeit für das Importsystem, daraus zu schließen, dass eine solche Erweiterung sicher ist, sodass der Import fehlschlägt. Dieser Fall wird in Kompatibilität von Erweiterungsmodulen oben behandelt.

Pflegekräfte von Erweiterungsmodulen

Eine verwandte Überlegung ist, dass ein GIL pro Interpreter wahrscheinlich zu einer erhöhten Nutzung mehrerer Interpreter führen wird, insbesondere wenn PEP 554 akzeptiert wird. Einige Pfleger von großen Erweiterungsmodulen haben Bedenken hinsichtlich der erhöhten Belastung geäußert, die sie aufgrund der zunehmenden Nutzung mehrerer Interpreter erwarten.

Insbesondere die Aktivierung der Unterstützung für mehrere Interpreter wird erhebliche Arbeit für einige Erweiterungsmodule erfordern (wenn auch wahrscheinlich nicht für viele). Um diese Unterstützung hinzuzufügen, müssten die Pfleger eines solchen Moduls (oft Freiwillige) ihre normalen Prioritäten und Interessen beiseitelassen, um sich auf die Kompatibilität zu konzentrieren (siehe PEP 630).

Natürlich steht es den Pflegern von Erweiterungen frei, keine Unterstützung für die Verwendung in mehreren Interpretern hinzuzufügen. Benutzer werden jedoch zunehmend eine solche Unterstützung verlangen, insbesondere wenn die Funktion an Popularität gewinnt.

In beiden Fällen kann die Situation für Pfleger solcher Erweiterungen belastend sein, insbesondere wenn sie die Arbeit in ihrer Freizeit erledigen. Die von ihnen geäußerten Bedenken sind verständlich, und wir behandeln die Teillösung in den Abschnitten Beschränkung von Erweiterungsmodulen und Kompatibilität von Erweiterungsmodulen.

Alternative Python-Implementierungen

Andere Python-Implementierungen sind nicht verpflichtet, Unterstützung für mehrere Interpreter im selben Prozess bereitzustellen (obwohl einige dies bereits tun).

Sicherheitsimplikationen

Es gibt keine bekannten Auswirkungen auf die Sicherheit durch diesen Vorschlag.

Wartbarkeit

Einerseits hat dieser Vorschlag bereits eine Reihe von Verbesserungen motiviert, die CPython *wartbarer* machen. Dies wird voraussichtlich so weitergehen. Andererseits hat die zugrunde liegende Arbeit bereits verschiedene bestehende Mängel in der Laufzeit aufgedeckt, die behoben werden mussten. Dies wird voraussichtlich ebenfalls weitergehen, da mehrere Interpreter zunehmend genutzt werden. Ansonsten sollte keine signifikante Auswirkung auf die Wartbarkeit zu erwarten sein, sodass der Nettoeffekt positiv sein sollte.

Performance

Die Arbeit zur Konsolidierung von Globals hat bereits eine Reihe von Verbesserungen für die Leistung von CPython gebracht, sowohl durch Beschleunigung als auch durch geringeren Speicherverbrauch, und dies sollte sich fortsetzen. Die Leistungsvorteile eines GIL pro Interpreter wurden speziell nicht untersucht. Zumindest wird nicht erwartet, dass CPython dadurch langsamer wird (solange die Interpreter ausreichend isoliert sind). Und offensichtlich ermöglicht es eine Vielzahl von Multi-Core-Parallelität in Python-Code.

Wie man das lehrt

Im Gegensatz zu PEP 554 handelt es sich hier um eine fortgeschrittene Funktion für eine kleine Gruppe von Benutzern der C-API. Es wird nicht erwartet, dass die Besonderheiten der API oder ihre direkte Anwendung gelehrt werden.

Das gesagt, wenn es gelehrt würde, würde es auf das Folgende hinauslaufen

Zusätzlich zu Py_NewInterpreter() können Sie Py_NewInterpreterFromConfig() verwenden, um einen Interpreter zu erstellen. Die Konfiguration, die Sie übergeben, gibt an, wie sich dieser Interpreter verhalten soll.

Darüber hinaus müssen die Wartungsbeauftragten von Erweiterungsmodulen, die isolierte Interpreter erstellen, wahrscheinlich die Konsequenzen eines GIL pro Interpreter für ihre Benutzer erklären. Das Erste, was erklärt werden muss, ist, was PEP 554 über das Nebenläufigkeitsmodell lehrt, das isolierte Interpreter ermöglichen. Daraus ergibt sich der Punkt, dass Python-Software, die dieses Nebenläufigkeitsmodell verwendet, dann Multi-Core-Parallelität nutzen kann, was derzeit durch den GIL verhindert wird.

Referenzimplementierung

<TBD>

Offene Fragen

  • Sind wir damit einverstanden, dass "mem" und "object" Allocators threadsicher sein müssen?
  • Wie würde sich ein tracemalloc-Modul pro Interpreter zu globalen Allokatoren verhalten?
  • Wäre das faulthandler-Modul auf den Hauptinterpreter beschränkt (wie das signal-Modul) oder würden wir diesen globalen Zustand zwischen Interpretern lecken (geschützt durch ein granuläres Schloss)?
  • Sollte eine informelle PEP mit allen relevanten Informationen aus dem Abschnitt "Consolidating Runtime Global State" ausgegliedert werden?
  • Wie wahrscheinlich ist es, dass ein Modul unter mehreren Interpretern (Isolation) funktioniert, aber nicht unter einem GIL pro Interpreter? (Siehe Thread-Sicherheit von Erweiterungsmodulen.)
  • Wenn dies wahrscheinlich genug ist, was können wir tun, um die Wartungsbeauftragten von Erweiterungen zu unterstützen, die Probleme zu mildern und die Nutzung unter einem GIL pro Interpreter zu genießen?
  • Wie wäre ein besserer (beängstigender klingender) Name für allow_all_extensions?

Aufgeschobene Funktionalität

  • PyInterpreterConfig Option, um den Interpreter immer in einem neuen Thread auszuführen
  • PyInterpreterConfig Option, um einem Interpreter einen "Haupt"-Thread zuzuweisen und nur in diesem Thread auszuführen

Abgelehnte Ideen

<TBD>

Zusätzlicher Kontext

Gemeinsame Nutzung globaler Objekte

Wir teilen einige globale Objekte zwischen Interpretern. Dies ist ein Implementierungsdetail und bezieht sich mehr auf die Konsolidierung von Globals als auf diesen Vorschlag, aber es ist ein ausreichend wichtiges Detail, um es hier zu erklären.

Die Alternative besteht darin, niemals Objekte zwischen Interpretern zu teilen. Um dies zu erreichen, müssten wir das Schicksal all unserer statischen Typen klären und uns mit Kompatibilitätsproblemen für die vielen Objekte befassen, die in der öffentlichen C-API exponiert werden.

Dieser Ansatz bringt eine erhebliche zusätzliche Komplexität und ein höheres Risiko mit sich, obwohl Prototypen gültige Lösungen gezeigt haben. Außerdem würde er wahrscheinlich zu einer Leistungseinbuße führen.

Unsterbliche Objekte ermöglichen es uns, die ansonsten unveränderlichen globalen Objekte zu teilen. Auf diese Weise vermeiden wir zusätzliche Kosten.

Objekte, die in der C-API verfügbar sind

Die C-API (einschließlich der begrenzten API) exponiert alle eingebauten Typen, einschließlich der eingebauten Ausnahmen, sowie die eingebauten Singletons. Die Ausnahmen werden als PyObject * exponiert, aber der Rest wird als statische Werte und nicht als Zeiger exponiert. Dies war eines der wenigen nicht-trivialen Probleme, die wir für den GIL pro Interpreter lösen mussten.

Mit unsterblichen Objekten ist dies kein Problem.

Konsolidierung des globalen Laufzeitstatus

Wie bereits in CPython Runtime State oben erwähnt, gibt es eine aktive Anstrengung (getrennt von dieser PEP), den globalen Zustand von CPython in die Struktur _PyRuntimeState zu konsolidieren. Fast die gesamte Arbeit besteht darin, diesen Zustand von globalen Variablen zu verschieben. Das Projekt ist für diesen Vorschlag besonders relevant, daher einige zusätzliche Details unten.

Vorteile der Konsolidierung

Die Konsolidierung der Globals hat eine Vielzahl von Vorteilen

  • reduziert die Anzahl der C-Globals erheblich (Best Practice für C-Code)
  • der Schritt lenkt die Aufmerksamkeit auf Laufzeitzustände, die instabil oder fehlerhaft sind
  • fördert mehr Konsistenz bei der Verwendung von Laufzeitzuständen
  • erleichtert die Entdeckung/Identifizierung des CPython-Laufzeitzustands
  • erleichtert die statische Zuweisung von Laufzeitzuständen auf konsistente Weise
  • bessere Speicherlokalität für Laufzeitzustände

Darüber hinaus gelten alle in Indirect Benefits oben aufgeführten Vorteile auch hier, und dieselben dort aufgeführten Projekte profitieren.

Umfang der Arbeiten

Die Anzahl der zu verschiebenden globalen Variablen ist ausreichend groß, aber die meisten sind Python-Objekte, die in großen Gruppen behandelt werden können (wie Py_IDENTIFIER). In fast allen Fällen ist das Verschieben dieser Globals in den Interpreter hochgradig mechanisch. Dies erfordert keine Raffinesse, sondern jemanden, der die Zeit investiert.

Zu verschiebender Status

Die verbleibenden globalen Variablen lassen sich wie folgt kategorisieren

  • globale Objekte
    • statische Typen (einschl. Ausnahmetypen)
    • nicht-statische Typen (einschl. Heap-Typen, Structseq-Typen)
    • Singletons (statisch)
    • Singletons (einmal initialisiert)
    • Zwischengespeicherte Objekte
  • Nicht-Objekte
    • werden nach der Initialisierung nicht (oder unwahrscheinlich) geändert
    • nur im Hauptthread verwendet
    • träge initialisiert
    • vorab zugewiesene Puffer
    • Zustand

Diese Globals sind auf den Kern-Runtime, die Builtin-Module und die Stdlib-Erweiterungsmodule verteilt.

Für eine Aufschlüsselung der verbleibenden Globals führen Sie

./python Tools/c-analyzer/table-file.py Tools/c-analyzer/cpython/globals-to-fix.tsv

Bereits abgeschlossene Arbeiten

Wie erwähnt, läuft diese Arbeit seit vielen Jahren. Hier sind einige der Dinge, die bereits getan wurden

  • Bereinigung der Runtime-Initialisierung (siehe PEP 432 / PEP 587)
  • Mechanismen zur Isolierung von Erweiterungsmodulen (siehe PEP 384 / PEP 3121 / PEP 489)
  • Isolierung für viele Builtin-Module
  • Isolierung für viele Stdlib-Erweiterungsmodule
  • Hinzufügung von _PyRuntimeState
  • keine _Py_IDENTIFIER() mehr
  • statisch zugewiesen
    • leerer String
    • String-Literale
    • Bezeichner
    • Latein-1-Strings
    • Bytes der Länge 1
    • leeres Tupel

Werkzeuge

Wie bereits angedeutet, gibt es mehrere Werkzeuge, um die Globals zu identifizieren und darüber nachzudenken.

  • Tools/c-analyzer/cpython/globals-to-fix.tsv - die Liste der verbleibenden Globals
  • Tools/c-analyzer/c-analyzer.py
    • analyze - identifiziert alle Globals
    • check - schlägt fehl, wenn es nicht ignorierte, nicht unterstützte Globals gibt
  • Tools/c-analyzer/table-file.py - fasst die bekannten Globals zusammen

Außerdem ist die Prüfung auf nicht unterstützte Globals in CI integriert, damit keine neuen Globals versehentlich hinzugefügt werden.

Globale Objekte

Globale Objekte, die sicher (ohne GIL) zwischen Interpretern geteilt werden können, können auf _PyRuntimeState verbleiben. Nicht nur muss das Objekt effektiv unveränderlich sein (z. B. Singletons, Strings), sondern auch die Referenzanzahl darf sich nicht ändern, um sicher zu sein. Unsterblichkeit (PEP 683) bietet dies. (Die Alternative ist, dass keine Objekte geteilt werden, was die Lösung erheblich verkompliziert, insbesondere für die Objekte die in der öffentlichen C-API exponiert werden.)

Builtin-statische Typen sind ein Sonderfall von globalen Objekten, die geteilt werden. Sie sind effektiv unveränderlich, mit einer Ausnahme: __subclasses__ (auch bekannt als tp_subclasses). Wir gehen davon aus, dass sich nichts anderes an einem eingebauten Typ ändern wird, nicht einmal der Inhalt von __dict__ (auch bekannt als tp_dict).

__subclasses__ für die eingebauten Typen werden behandelt, indem es zu einem Getter gemacht wird, der auf der aktuellen PyInterpreterState für diesen Typ nachschlägt.

Referenzen

Verwandt

  • PEP 384 "Defining a Stable ABI"
  • PEP 432 "Restructuring the CPython startup sequence"
  • PEP 489 "Multi-phase extension module initialization"
  • PEP 554 "Multiple Interpreters in the Stdlib"
  • PEP 573 "Module State Access from C Extension Methods"
  • PEP 587 "Python Initialization Configuration"
  • PEP 630 "Isolating Extension Modules"
  • PEP 683 "Immortal Objects, Using a Fixed Refcount"
  • PEP 3121 "Extension Module Initialization and Finalization"

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

Zuletzt geändert: 2024-06-04 17:05:36 GMT