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

Python Enhancement Proposals

PEP 734 – Mehrere Interpreter in der Standardbibliothek

Autor:
Eric Snow <ericsnowcurrently at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Erstellt:
06-Nov-2023
Python-Version:
3.14
Post-History:
14-Dez-2023
Ersetzt:
554
Resolution:
05-Jun-2025

Inhaltsverzeichnis

Wichtig

Diese PEP ist ein historisches Dokument. Die aktuelle, kanonische Dokumentation finden Sie nun unter concurrent.interpreters.

×

Siehe PEP 1, um Änderungen vorzuschlagen.

Hinweis

Diese PEP ist im Wesentlichen eine Fortsetzung von PEP 554. Dieses Dokument war über 7 Jahre Diskussion hinweg stark mit zusätzlichen Informationen angewachsen. Diese PEP ist eine Reduzierung auf die wesentlichen Informationen. Ein Großteil dieser zusätzlichen Informationen ist immer noch gültig und nützlich, nur nicht im unmittelbaren Kontext des hier spezifischen Vorschlags.

Hinweis

Diese PEP wurde mit der Auflage angenommen, dass der Name in concurrent.interpreters geändert wird.

Zusammenfassung

Diese PEP schlägt die Hinzufügung eines neuen Moduls, interpreters, vor, um das Untersuchen, Erstellen und Ausführen von Code in mehreren Interpretern im aktuellen Prozess zu unterstützen. Dies beinhaltet Interpreter-Objekte, die die zugrunde liegenden Interpreter darstellen. Das Modul wird auch eine einfache Queue-Klasse für die Kommunikation zwischen Interpretern bereitstellen. Schließlich fügen wir einen neuen concurrent.futures.InterpreterPoolExecutor hinzu, der auf dem Modul interpreters basiert.

Einleitung

Grundsätzlich ist ein „Interpreter“ die Sammlung (im Wesentlichen) aller Laufzeitzustände, die Python-Threads teilen müssen. Betrachten wir also zuerst Threads. Dann kehren wir zu den Interpretern zurück.

Threads und Thread-Zustände

Ein Python-Prozess wird einen oder mehrere Betriebssystem-Threads haben, die Python-Code ausführen (oder auf andere Weise mit der C-API interagieren). Jeder dieser Threads interagiert mit der CPython-Laufzeitumgebung über seinen eigenen Thread-Zustand (PyThreadState), der alle für diesen Thread eindeutigen Laufzeitzustände enthält. Es gibt auch einige Laufzeitzustände, die zwischen mehreren Betriebssystem-Threads geteilt werden.

Jeder Betriebssystem-Thread kann seinen aktuellen Thread-Zustand wechseln, solange es sich nicht um einen handelt, den ein anderer Betriebssystem-Thread bereits verwendet (oder verwendet hat). Dieser „aktuelle“ Thread-Zustand wird von der Laufzeitumgebung in einer Thread-lokalen Variablen gespeichert und kann explizit mit PyThreadState_Get() abgerufen werden. Er wird automatisch für den anfänglichen („Haupt“-)Betriebssystem-Thread und für threading.Thread-Objekte gesetzt. Aus der C-API wird er durch PyThreadState_Swap() gesetzt (und gelöscht) und kann durch PyGILState_Ensure() gesetzt werden. Die meisten C-API-Aufrufe erfordern einen aktuellen Thread-Zustand, entweder implizit abgerufen oder als Argument übergeben.

Die Beziehung zwischen Betriebssystem-Threads und Thread-Zuständen ist eins-zu-viele. Jeder Thread-Zustand ist mit höchstens einem Betriebssystem-Thread verbunden und speichert dessen Thread-ID. Ein Thread-Zustand wird niemals für mehr als einen Betriebssystem-Thread verwendet. In der anderen Richtung kann ein Betriebssystem-Thread jedoch mehr als einen Thread-Zustand zugeordnet haben, wobei jedoch nur einer aktuell sein kann.

Wenn es mehr als einen Thread-Zustand für einen Betriebssystem-Thread gibt, wird PyThreadState_Swap() in diesem Betriebssystem-Thread verwendet, um zwischen ihnen zu wechseln, wobei der angeforderte Thread-Zustand zum aktuellen wird. Was auch immer in dem Thread lief, der den alten Thread-Zustand verwendet hat, wird effektiv pausiert, bis dieser Thread-Zustand wieder eingeschaltet wird.

Interpreter-Zustände

Wie bereits erwähnt, gibt es einige Laufzeitzustände, die mehrere Betriebssystem-Threads teilen. Einige davon werden vom Modul sys bereitgestellt, obwohl viele intern verwendet und nicht explizit oder nur über die C-API freigegeben werden.

Dieser gemeinsame Zustand wird als Interpreter-Zustand (PyInterpreterState) bezeichnet. Wir werden ihn hier manchmal einfach als „Interpreter“ bezeichnen, obwohl dieser Begriff auch manchmal verwendet wird, um die ausführbare Datei python, die Python-Implementierung und den Bytecode-Interpreter (d. h. exec()/eval()) zu bezeichnen.

CPython unterstützt mehrere Interpreter im selben Prozess (auch bekannt als „Sub-Interpreter“) seit Version 1.5 (1997). Die Funktion ist über die C API verfügbar.

Interpreter und Threads

Thread-Zustände sind in etwa so mit Interpreter-Zuständen verbunden, wie Betriebssystem-Threads und Prozesse miteinander verbunden sind (auf hoher Ebene). Zunächst ist die Beziehung eins-zu-viele. Ein Thread-Zustand gehört zu einem einzigen Interpreter (und speichert einen Zeiger darauf). Dieser Thread-Zustand wird niemals für einen anderen Interpreter verwendet. In der anderen Richtung kann ein Interpreter jedoch null oder mehr zugeordnete Thread-Zustände haben. Der Interpreter gilt nur dann als aktiv in Betriebssystem-Threads, wenn einer seiner Thread-Zustände aktuell ist.

Interpreter werden über die C-API mit Py_NewInterpreterFromConfig() (oder Py_NewInterpreter(), einer leichten Wrapper-Funktion um Py_NewInterpreterFromConfig()) erstellt. Diese Funktion tut Folgendes:

  1. Erstellt einen neuen Interpreter-Zustand
  2. Erstellt einen neuen Thread-Zustand
  3. Setzt den Thread-Zustand auf aktuell (ein aktueller tstate wird für die Interpreter-Initialisierung benötigt)
  4. Initialisiert den Interpreter-Zustand unter Verwendung dieses Thread-Zustands
  5. Gibt den Thread-Zustand zurück (immer noch aktuell)

Beachten Sie, dass der zurückgegebene Thread-Zustand sofort verworfen werden kann. Es gibt keine Anforderung, dass ein Interpreter Thread-Zustände hat, außer sobald der Interpreter tatsächlich verwendet werden soll. Zu diesem Zeitpunkt muss er im aktuellen Betriebssystem-Thread aktiviert werden.

Um einen vorhandenen Interpreter im aktuellen Betriebssystem-Thread zu aktivieren, stellt der Benutzer der C-API zuerst sicher, dass der Interpreter einen entsprechenden Thread-Zustand hat. Dann wird PyThreadState_Swap() wie gewohnt mit diesem Thread-Zustand aufgerufen. Wenn der Thread-Zustand eines anderen Interpreters bereits aktuell war, wird er wie gewohnt ausgetauscht und die Ausführung dieses Interpreters im Betriebssystem-Thread ist somit effektiv pausiert, bis er wieder eingeschaltet wird.

Sobald ein Interpreter auf diese Weise im aktuellen Betriebssystem-Thread aktiviert ist, kann der Thread jede C-API aufrufen, wie z. B. PyEval_EvalCode() (d. h. exec()). Dies geschieht durch die Verwendung des aktuellen Thread-Zustands als Laufzeitkontext.

Der „Haupt“-Interpreter

Wenn ein Python-Prozess gestartet wird, erstellt er einen einzigen Interpreter-Zustand (den „Haupt“-Interpreter) mit einem einzigen Thread-Zustand für den aktuellen Betriebssystem-Thread. Die Python-Laufzeitumgebung wird dann damit initialisiert.

Nach der Initialisierung wird das Skript oder Modul oder die REPL damit ausgeführt. Diese Ausführung findet im __main__-Modul des Interpreters statt.

Wenn der Prozess die Ausführung des angeforderten Python-Codes oder der REPL im Haupt-Betriebssystem-Thread beendet, wird die Python-Laufzeitumgebung in diesem Thread mit dem Haupt-Interpreter finalisiert.

Die Finalisierung der Laufzeitumgebung hat nur geringe, indirekte Auswirkungen auf noch laufende Python-Threads, sei es im Haupt-Interpreter oder in Sub-Interpretern. Das liegt daran, dass sie sofort unbegrenzt wartet, bis alle Nicht-Daemon-Python-Threads beendet sind.

Während die C-API abgefragt werden kann, gibt es keinen Mechanismus, durch den ein Python-Thread direkt darüber informiert wird, dass die Finalisierung begonnen hat, außer vielleicht durch „atexit“-Funktionen, die mit threading._register_atexit() registriert wurden.

Alle verbleibenden Sub-Interpreter werden später selbst finalisiert, aber zu diesem Zeitpunkt sind sie in keinem Betriebssystem-Thread mehr aktuell.

Interpreter-Isolation

CPython-Interpreter sind für eine strikte Isolation voneinander konzipiert. Das bedeutet, dass Interpreter niemals Objekte teilen (außer in sehr spezifischen Fällen mit unsterblichen, unveränderlichen eingebauten Objekten). Jeder Interpreter hat seine eigenen Module (sys.modules), Klassen, Funktionen und Variablen. Selbst wenn zwei Interpreter dieselbe Klasse definieren, hat jeder seine eigene Kopie. Das Gleiche gilt für Zustände in C, einschließlich Erweiterungsmodulen. Die CPython C-API-Dokumentation erklärt mehr.

Insbesondere gibt es einige prozessweite Zustände, die Interpreter immer teilen werden, einige veränderlich und einige unveränderlich. Das Teilen von unveränderlichen Zuständen birgt wenige Probleme und bietet einige Vorteile (hauptsächlich Leistung). Alle gemeinsam genutzten veränderlichen Zustände erfordern jedoch eine spezielle Verwaltung, insbesondere im Hinblick auf Thread-Sicherheit, von denen einige vom Betriebssystem für uns übernommen werden.

Veränderlich

  • Datei-Deskriptoren
  • Low-Level-Umgebungsvariablen
  • Prozessspeicher (obwohl Allocatoren *isoliert* sind)
  • die Liste der Interpreter

Unveränderlich

  • eingebaute Typen (z. B. dict, bytes)
  • Singletons (z. B. None)
  • zugrunde liegende statische Moduldaten (z. B. Funktionen) für eingebaute/Erweiterungs-/eingefrorene Module

Bestehende Ausführungskomponenten

Es gibt eine Reihe bestehender Komponenten in Python, die zum Verständnis beitragen können, wie Code in einem Sub-Interpreter ausgeführt werden kann.

In CPython ist jede Komponente um eine der folgenden C-API-Funktionen (oder Varianten) aufgebaut:

  • PyEval_EvalCode(): Ausführen des Bytecode-Interpreters mit dem gegebenen Codeobjekt
  • PyRun_String(): Kompilieren + PyEval_EvalCode()
  • PyRun_File(): Lesen + Kompilieren + PyEval_EvalCode()
  • PyRun_InteractiveOneObject(): Kompilieren + PyEval_EvalCode()
  • PyObject_Call(): ruft PyEval_EvalCode() auf

builtins.exec()

Die eingebaute Funktion exec() kann zur Ausführung von Python-Code verwendet werden. Sie ist im Wesentlichen ein Wrapper um die C-API-Funktionen PyRun_String() und PyEval_EvalCode().

Hier sind einige relevante Eigenschaften der eingebauten Funktion exec():

  • Sie läuft im aktuellen Betriebssystem-Thread und pausiert alles, was dort lief. Dieses wird wieder aufgenommen, wenn exec() beendet ist. Keine anderen Betriebssystem-Threads werden beeinflusst. (Um den aktuellen Python-Thread nicht zu pausieren, führen Sie exec() in einem threading.Thread aus.)
  • Sie kann zusätzliche Threads starten, die sie nicht unterbrechen.
  • Sie wird gegen einen „Globals“-Namensraum (und einen „Locals“-Namensraum) ausgeführt. Auf Modulebene verwendet exec() standardmäßig das __dict__ des aktuellen Moduls (d. h. globals()). exec() verwendet diesen Namensraum unverändert und löscht ihn weder davor noch danach.
  • Sie gibt jede nicht abgefangene Ausnahme aus dem ausgeführten Code weiter. Die Ausnahme wird vom Aufruf von exec() in dem Python-Thread ausgelöst, der exec() ursprünglich aufgerufen hat.

Befehlszeile

Die CLI python bietet mehrere Möglichkeiten, Python-Code auszuführen. In jedem Fall wird sie einem entsprechenden C-API-Aufruf zugeordnet:

  • <keine Argumente>, -i - REPL ausführen (PyRun_InteractiveOneObject())
  • <Dateiname> - Skript ausführen (PyRun_File())
  • -c <Code> - den gegebenen Python-Code ausführen (PyRun_String())
  • -m Modul - das Modul als Skript ausführen (PyEval_EvalCode() über runpy._run_module_as_main())

In jedem Fall ist es im Wesentlichen eine Variante des Ausführens von exec() auf der obersten Ebene des __main__-Moduls des Haupt-Interpreters.

threading.Thread

Wenn ein Python-Thread gestartet wird, führt er die „target“-Funktion mit PyObject_Call() unter Verwendung eines neuen Thread-Zustands aus. Der Globals-Namensraum stammt von func.__globals__ und jede nicht abgefangene Ausnahme wird verworfen.

Motivation

Das Modul interpreters wird eine High-Level-Schnittstelle zur Unterstützung mehrerer Interpreter bereitstellen. Das Ziel ist es, die bestehende Multi-Interpreter-Unterstützung von CPython für Python-Code leichter zugänglich zu machen. Dies ist besonders relevant, da CPython einen Per-Interpreter-GIL hat (PEP 684) und Leute mehr daran interessiert sind, mehrere Interpreter zu verwenden.

Ohne ein Standardbibliotheksmodul sind Benutzer auf die C API beschränkt, was einschränkt, wie viel sie ausprobieren und von mehreren Interpretern profitieren können.

Das Modul wird einen grundlegenden Mechanismus für die Kommunikation zwischen Interpretern enthalten. Ohne einen sind mehrere Interpreter eine weitaus weniger nützliche Funktion.

Spezifikation

Das Modul wird

  • die bestehende Unterstützung für mehrere Interpreter freilegen
  • einen grundlegenden Mechanismus für die Kommunikation zwischen Interpretern einführen

Das Modul wird ein neues Low-Level-Modul _interpreters umschließen (ähnlich wie das Modul threading). Diese Low-Level-API ist jedoch nicht für die öffentliche Nutzung bestimmt und daher nicht Teil dieses Vorschlags.

Verwendung von Interpretern

Das Modul definiert die folgenden Funktionen:

  • get_current() -> Interpreter
    Gibt das Interpreter-Objekt für den aktuell ausgeführten Interpreter zurück.
  • list_all() -> list[Interpreter]
    Gibt das Interpreter-Objekt für jeden vorhandenen Interpreter zurück, unabhängig davon, ob er gerade in einem Betriebssystem-Thread läuft oder nicht.
  • create() -> Interpreter
    Erstellt einen neuen Interpreter und gibt das Interpreter-Objekt dafür zurück. Der Interpreter tut nichts von sich aus und ist nicht intrinsisch an einen Betriebssystem-Thread gebunden. Dies geschieht erst, wenn etwas tatsächlich im Interpreter ausgeführt wird (z. B. Interpreter.exec()), und nur, solange es ausgeführt wird. Der Interpreter kann Thread-Zustände haben oder nicht, die einsatzbereit sind, aber das ist rein ein internes Implementierungsdetail.

Interpreter-Objekte

Ein interpreters.Interpreter-Objekt, das den Interpreter (PyInterpreterState) mit der entsprechenden eindeutigen ID darstellt. Es wird nur ein Objekt für jeden gegebenen Interpreter geben.

Wenn der Interpreter mit interpreters.create() erstellt wurde, wird er zerstört, sobald alle Interpreter-Objekte mit seiner ID (über alle Interpreter hinweg) gelöscht wurden.

Interpreter-Objekte können andere Interpreter darstellen als diejenigen, die von interpreters.create() erstellt wurden. Beispiele hierfür sind der Haupt-Interpreter (erstellt durch die Python-Laufzeitinitialisierung) und solche, die über die C-API mit Py_NewInterpreter() erstellt wurden. Solche Interpreter-Objekte können nicht mit ihren entsprechenden Interpretern interagieren, z. B. über Interpreter.exec() (obwohl wir dies in Zukunft lockern könnten).

Attribute und Methoden

  • id
    (schreibgeschützt) Eine nicht-negative int, die den Interpreter identifiziert, den diese Interpreter-Instanz darstellt. Konzeptionell ist dies ähnlich einer Prozess-ID.
  • __hash__()
    Gibt den Hash der id des Interpreters zurück. Dies ist derselbe wie der Hash des ganzzahligen Werts der ID.
  • is_running() -> bool
    Gibt True zurück, wenn der Interpreter gerade Code in seinem __main__-Modul ausführt. Dies schließt Sub-Threads aus.

    Es bezieht sich nur darauf, ob ein Betriebssystem-Thread ein Skript (Code) im __main__-Modul des Interpreters ausführt. Das bedeutet im Grunde, ob Interpreter.exec() in einem Betriebssystem-Thread läuft. Code, der in Sub-Threads läuft, wird ignoriert.

  • prepare_main(**kwargs)
    Bindet ein oder mehrere Objekte im __main__-Modul des Interpreters.

    Die Namen der Schlüsselwörter werden als Attributnamen verwendet. Für die meisten Objekte wird eine Kopie im Interpreter gebunden, wobei Pickling dazwischen verwendet wird. Für einige Objekte, wie z. B. memoryview, werden die zugrunde liegenden Daten zwischen den Interpretern geteilt. Siehe Teilbare Objekte.

    prepare_main() ist hilfreich zur Initialisierung der Globals für einen Interpreter, bevor Code in ihm ausgeführt wird.

  • exec(code, /)
    Führt den gegebenen Quellcode im Interpreter aus (im aktuellen Betriebssystem-Thread) unter Verwendung seines __main__-Moduls. Es gibt nichts zurück.

    Dies ist im Wesentlichen äquivalent zum Wechseln zu diesem Interpreter im aktuellen Betriebssystem-Thread und dann zum Aufrufen der eingebauten Funktion exec() unter Verwendung des __main__-Moduls dieses Interpreters als Globals und Locals.

    Der im aktuellen Betriebssystem-Thread laufende Code (ein anderer Interpreter) wird bis zur Beendigung von Interpreter.exec() effektiv pausiert. Um dies zu vermeiden, erstellen Sie einen neuen threading.Thread und rufen Sie Interpreter.exec() darin auf (wie Interpreter.call_in_thread() es tut).

    Interpreter.exec() setzt den Zustand des Interpreters oder das __main__-Modul weder davor noch danach zurück, sodass jeder aufeinanderfolgende Aufruf dort anknüpft, wo der letzte aufgehört hat. Dies kann nützlich sein, um etwas Code zur Initialisierung eines Interpreters (z. B. mit Imports) auszuführen, bevor später eine wiederholte Aufgabe durchgeführt wird.

    Wenn es eine nicht abgefangene Ausnahme gibt, wird sie in den aufrufenden Interpreter als ExecutionFailed weitergegeben. Die vollständige Fehleranzeige der ursprünglichen Ausnahme, generiert relativ zum aufgerufenen Interpreter, wird auf der weitergegebenen ExecutionFailed beibehalten. Dies beinhaltet den vollständigen Traceback mit allen zusätzlichen Informationen wie Syntaxfehlerdetails und verketteten Ausnahmen. Wenn die ExecutionFailed nicht abgefangen wird, wird die vollständige Fehleranzeige angezeigt, ähnlich wie sie angezeigt würde, wenn die weitergegebene Ausnahme im Haupt-Interpreter ausgelöst und nicht abgefangen worden wäre. Das Vorhandensein des vollständigen Tracebacks ist besonders beim Debugging nützlich.

    Wenn eine Ausnahmeweitergabe nicht gewünscht ist, sollte ein explizites try-except um den *an* Interpreter.exec() übergebenen *Code* verwendet werden. Ebenso muss jede Fehlerbehandlung, die auf spezifischen Informationen aus der Ausnahme basiert, ein explizites try-except um den gegebenen *Code* verwenden, da ExecutionFailed diese Informationen nicht beibehält.

  • call(callable, /)
    Ruft das aufrufbare Objekt im Interpreter auf. Der Rückgabewert wird verworfen. Wenn das aufrufbare Objekt eine Ausnahme auslöst, wird sie als ExecutionFailed-Ausnahme weitergegeben, auf die gleiche Weise wie bei Interpreter.exec().

    Derzeit werden nur einfache Funktionen unterstützt, und zwar nur solche, die keine Argumente annehmen und keine Cell-Variablen haben. Freie Globale werden gegen das __main__-Modul des Zielinterpreters aufgelöst.

    In Zukunft können wir die Unterstützung für Argumente, Closures und eine breitere Palette von aufrufbaren Objekten hinzufügen, zumindest teilweise über Pickling. Wir können auch erwägen, den Rückgabewert nicht zu verwerfen. Die anfänglichen Einschränkungen sind vorhanden, um uns zu ermöglichen, die grundlegende Funktionalität des Moduls schneller an die Benutzer weiterzugeben.

  • call_in_thread(callable, /) -> threading.Thread
    Im Wesentlichen wird Interpreter.call() in einem neuen Thread angewendet. Rückgabewerte werden verworfen und Ausnahmen nicht weitergegeben.

    call_in_thread() ist ungefähr gleichbedeutend mit

    def task():
        interp.call(func)
    t = threading.Thread(target=task)
    t.start()
    
  • close()
    Zerstört den zugrunde liegenden Interpreter.

Kommunikation zwischen Interpretern

Das Modul führt einen grundlegenden Kommunikationsmechanismus über spezielle Warteschlangen ein.

Es gibt interpreters.Queue-Objekte, aber sie leiten nur die tatsächliche Datenstruktur weiter: eine unbegrenzte FIFO-Warteschlange, die außerhalb eines einzelnen Interpreters existiert. Diese Warteschlangen verfügen über spezielle Vorkehrungen, um Objektdaten sicher zwischen Interpretern zu übergeben, ohne die Interpreter-Isolation zu verletzen. Dazu gehört auch Thread-Sicherheit.

Wie bei anderen Warteschlangen in Python wird bei jeder „put“-Operation das Objekt am Ende hinzugefügt und bei jeder „get“-Operation das nächste vom Anfang entfernt. Jedes hinzugefügte Objekt wird in der Reihenfolge entnommen, in der es eingefügt wurde.

Jedes Objekt, das mit Pickling serialisiert werden kann, kann über eine interpreters.Queue gesendet werden.

Beachten Sie, dass die tatsächlichen Objekte nicht gesendet werden, sondern ihre zugrunde liegenden Daten. Das resultierende Objekt ist strikt äquivalent zum Original. Für die meisten Objekte werden die zugrunde liegenden Daten serialisiert (z. B. mit Pickling). In einigen Fällen, wie bei memoryview, werden die zugrunde liegenden Daten ohne Serialisierung gesendet (und geteilt). Siehe Teilbare Objekte.

Das Modul definiert die folgenden Funktionen:

  • create_queue(maxsize=0) -> Queue
    Erstellt eine neue Warteschlange. Wenn maxsize null oder negativ ist, ist die Warteschlange unbegrenzt.

Warteschlangen-Objekte

interpreters.Queue-Objekte fungieren als Proxies für die zugrunde liegenden, für mehrere Interpreter sicheren Warteschlangen, die vom Modul interpreters bereitgestellt werden. Jedes Queue-Objekt repräsentiert die Warteschlange mit der entsprechenden eindeutigen ID. Es wird nur ein Objekt für jede gegebene Warteschlange geben.

Queue implementiert alle Methoden von queue.Queue mit Ausnahme von task_done() und join(), daher ist sie ähnlich wie asyncio.Queue und multiprocessing.Queue.

Attribute und Methoden

  • id
    (schreibgeschützt) Eine nicht-negative int, die die entsprechende Warteschlange zwischen Interpretern identifiziert. Konzeptionell ist dies ähnlich dem Dateideskriptor, der für eine Pipe verwendet wird.
  • maxsize
    (schreibgeschützt) Anzahl der in der Warteschlange zulässigen Elemente. Null bedeutet „unbegrenzt“.
  • __hash__()
    Gibt den Hash der id der Warteschlange zurück. Dies ist derselbe wie der Hash des ganzzahligen Werts der ID.
  • empty()
    Gibt True zurück, wenn die Warteschlange leer ist, andernfalls False.

    Dies ist nur eine Momentaufnahme des Zustands zum Zeitpunkt des Aufrufs. Andere Threads oder Interpreter können dazu führen, dass sich dies ändert.

  • full()
    Gibt True zurück, wenn sich maxsize Elemente in der Warteschlange befinden.

    Wenn die Warteschlange mit maxsize=0 (Standard) initialisiert wurde, gibt full() niemals True zurück.

    Dies ist nur eine Momentaufnahme des Zustands zum Zeitpunkt des Aufrufs. Andere Threads oder Interpreter können dazu führen, dass sich dies ändert.

  • qsize()
    Gibt die Anzahl der Elemente in der Warteschlange zurück.

    Dies ist nur eine Momentaufnahme des Zustands zum Zeitpunkt des Aufrufs. Andere Threads oder Interpreter können dazu führen, dass sich dies ändert.

  • put(obj, timeout=None)
    Fügt das Objekt zur Warteschlange hinzu.

    Wenn maxsize > 0 und die Warteschlange voll ist, blockiert dies, bis ein freier Platz verfügbar ist. Wenn timeout eine positive Zahl ist, blockiert es nur mindestens so viele Sekunden und löst dann interpreters.QueueFull aus. Andernfalls blockiert es unendlich.

    Fast alle Objekte können über die Warteschlange gesendet werden. In einigen Fällen, wie bei memoryview, werden die zugrunde liegenden Daten tatsächlich geteilt und nicht nur kopiert. Siehe Teilbare Objekte.

    Wenn sich ein Objekt noch in der Warteschlange befindet und der Interpreter, der es in die Warteschlange gelegt hat (d. h. zu dem es gehört), zerstört wird, wird das Objekt sofort aus der Warteschlange entfernt. (Wir können später eine Option hinzufügen, um das entfernte Objekt in der Warteschlange durch ein Sentinel zu ersetzen oder eine Ausnahme für den entsprechenden get()-Aufruf auszulösen.)

  • put_nowait(obj
    Ähnlich wie put(), aber effektiv mit einem sofortigen Timeout. Wenn die Warteschlange voll ist, löst sie daher sofort interpreters.QueueFull aus.
  • get(timeout=None) -> object
    Entnimmt das nächste Objekt aus der Warteschlange und gibt es zurück. Blockiert, solange die Warteschlange leer ist. Wenn ein positiver timeout angegeben ist und innerhalb dieser Sekunden kein Objekt zur Warteschlange hinzugefügt wurde, wird interpreters.QueueEmpty ausgelöst.
  • get_nowait() -> object
    Ähnlich wie get(), aber ohne zu blockieren. Wenn die Warteschlange nicht leer ist, wird das nächste Element zurückgegeben. Andernfalls wird interpreters.QueueEmpty ausgelöst.

Teilbare Objekte

Ein „teilbares“ Objekt ist ein Objekt, das von einem Interpreter zu einem anderen übertragen werden kann. Das Objekt wird von den Interpretern nicht tatsächlich direkt geteilt. Das geteilte Objekt sollte jedoch so behandelt werden, als *würde* es direkt geteilt, mit Vorbehalten bezüglich der Veränderbarkeit.

Alle Objekte, die „pickled“ werden können, sind teilbar. Daher ist fast jedes Objekt teilbar. interpreters.Queue-Objekte sind ebenfalls teilbar.

In fast jedem Fall, in dem ein Objekt an einen Interpreter gesendet wird, sei es mit interp.prepare_main() oder queue.put(), wird nicht das tatsächliche Objekt gesendet. Stattdessen werden die zugrunde liegenden Daten des Objekts gesendet. Für die meisten Objekte wird das Objekt „pickled“ und der empfangende Interpreter entpickelt es.

Eine bemerkenswerte Ausnahme bilden Objekte, die das „Buffer“-Protokoll implementieren, wie z. B. memoryview. Ihre zugrunde liegende Py_buffer wird tatsächlich zwischen den Interpretern geteilt. interp.prepare_main() und queue.get() umschließen den Buffer in einem neuen memoryview-Objekt.

Für die meisten veränderbaren Objekte wird, wenn eines an einen anderen Interpreter gesendet wird, eine Kopie erstellt. Daher werden Änderungen am Original oder an der Kopie niemals mit dem anderen synchronisiert. Veränderbare Objekte, die durch „Pickling“ geteilt werden, fallen in diese Kategorie. interpreters.Queue und Objekte, die das Buffer-Protokoll implementieren, sind jedoch bemerkenswerte Fälle, in denen die zugrunde liegenden Daten zwischen den Interpretern *geteilt* werden, sodass Objekte synchron bleiben.

Wenn Interpreter tatsächlich veränderbare Daten teilen, besteht immer die Gefahr von Datenrennen. Cross-Interpreter-Sicherheit, einschließlich Thread-Sicherheit, ist ein grundlegendes Merkmal von interpreters.Queue.

Das Buffer-Protokoll (d. h. Py_buffer) bietet jedoch keine nativen Vorkehrungen gegen Datenrennen. Stattdessen ist der Benutzer für die Verwaltung der Thread-Sicherheit verantwortlich, sei es durch Weitergabe eines Tokens über eine Warteschlange zur Anzeige der Sicherheit (siehe Synchronisation) oder durch Zuweisung von Sub-Bereich-Exklusivität an einzelne Interpreter.

Die meisten Objekte werden über Warteschlangen (interpreters.Queue) geteilt, da Interpreter Informationen untereinander austauschen. Seltener werden Objekte über prepare_main() geteilt, um einen Interpreter vor der Ausführung von Code in ihm einzurichten. prepare_main() ist jedoch die primäre Methode, um Warteschlangen zu teilen und einem anderen Interpreter ein Mittel zur weiteren Kommunikation zur Verfügung zu stellen.

Synchronisation

Es gibt Situationen, in denen zwei Interpreter synchronisiert werden sollten. Dies kann die gemeinsame Nutzung einer Ressource, die Verwaltung von Workern oder die Beibehaltung der sequentiellen Konsistenz beinhalten.

In der Thread-Programmierung sind die typischen Synchronisationsprimitive Typen wie Mutexe. Das Modul threading stellt mehrere zur Verfügung. Interpreter können jedoch keine Objekte teilen, was bedeutet, dass sie keine threading.Lock-Objekte teilen können.

Das Modul interpreters stellt keine solchen dedizierten Synchronisationsprimitive bereit. Stattdessen bieten interpreters.Queue-Objekte alles, was man benötigt.

Zum Beispiel kann eine Warteschlange verwendet werden, um eine gemeinsam genutzte Ressource zu verwalten, auf die zugegriffen werden muss, wobei die Interpreter ein Objekt weitergeben, um anzuzeigen, wer die Ressource nutzen darf.

import interpreters
from mymodule import load_big_data, check_data

numworkers = 10
control = interpreters.create_queue()
data = memoryview(load_big_data())

def worker():
    interp = interpreters.create()
    interp.prepare_main(control=control, data=data)
    interp.exec("""if True:
        from mymodule import edit_data
        while True:
            token = control.get()
            edit_data(data)
            control.put(token)
        """)
threads = [threading.Thread(target=worker) for _ in range(numworkers)]
for t in threads:
    t.start()

token = 'football'
control.put(token)
while True:
    control.get()
    if not check_data(data):
        break
    control.put(token)

Ausnahmen

  • InterpreterError
    Zeigt an, dass ein fehlerhafter Interpreter-bezogener Vorgang aufgetreten ist.

    Diese Ausnahme ist eine Unterklasse von Exception.

  • InterpreterNotFoundError
    Ausgelöst von Interpreter-Methoden, nachdem der zugrunde liegende Interpreter zerstört wurde, z. B. über die C-API.

    Diese Ausnahme ist eine Unterklasse von InterpreterError.

  • ExecutionFailed
    Ausgelöst von Interpreter.exec() und Interpreter.call(), wenn eine nicht abgefangene Ausnahme auftritt. Die Fehlerausgabe für diese Ausnahme enthält den Traceback der nicht abgefangenen Ausnahme, der nach der normalen Fehlerausgabe angezeigt wird, ähnlich wie bei ExceptionGroup.

    Attribute

    • type - eine Darstellung der Klasse der ursprünglichen Ausnahme, mit __name__, __module__ und __qualname__ Attributen.
    • msg - str(exc) der ursprünglichen Ausnahme
    • snapshot - ein traceback.TracebackException Objekt für die ursprüngliche Ausnahme

    Diese Ausnahme ist eine Unterklasse von InterpreterError.

  • QueueError
    Zeigt an, dass ein fehlerhafter Warteschlangen-bezogener Vorgang aufgetreten ist.

    Diese Ausnahme ist eine Unterklasse von Exception.

  • QueueNotFoundError
    Ausgelöst von interpreters.Queue-Methoden, nachdem die zugrunde liegende Warteschlange zerstört wurde.

    Diese Ausnahme ist eine Unterklasse von QueueError.

  • QueueEmpty
    Ausgelöst von Queue.get() (oder get_nowait() ohne Standardwert), wenn die Warteschlange leer ist.

    Diese Ausnahme ist eine Unterklasse sowohl von QueueError als auch von der Standardbibliothek queue.Empty.

  • QueueFull
    Ausgelöst von Queue.put() (mit einem Timeout) oder put_nowait(), wenn die Warteschlange bereits ihre maximale Größe erreicht hat.

    Diese Ausnahme ist eine Unterklasse sowohl von QueueError als auch von der Standardbibliothek queue.Empty.

InterpreterPoolExecutor

Zusammen mit dem neuen Modul interpreters wird es einen neuen concurrent.futures.InterpreterPoolExecutor geben. Er wird ein Derivat von ThreadPoolExecutor sein, bei dem jeder Worker in seinem eigenen Thread ausgeführt wird, aber jeder mit seinem eigenen Subinterpreter.

Wie die anderen Executors unterstützt InterpreterPoolExecutor aufrufbare Funktionen für Aufgaben und für die Initialisierung. Ebenso wie bei den anderen Executors sind die Argumente in beiden Fällen weitgehend uneingeschränkt. Die aufrufbaren Funktionen und Argumente werden typischerweise serialisiert, wenn sie an den Interpreter eines Workers gesendet werden, z. B. mit Pickle, wie ProcessPoolExecutor funktioniert. Dies steht im Gegensatz zu Interpreter.call(), das (zumindest anfänglich) viel eingeschränkter sein wird.

Die Kommunikation zwischen Workern oder zwischen dem Executor (oder generell seinem Interpreter) und den Workern kann weiterhin über interpreters.Queue-Objekte erfolgen, die mit dem Initialisierer gesetzt werden.

sys.implementation.supports_isolated_interpreters

Python-Implementierungen sind nicht verpflichtet, Subinterpreter zu unterstützen, obwohl die meisten großen dies tun. Wenn eine Implementierung sie unterstützt, wird sys.implementation.supports_isolated_interpreters auf True gesetzt. Andernfalls wird sie auf False gesetzt. Wenn das Feature nicht unterstützt wird, löst der Import des Moduls interpreters einen ImportError aus.

Beispiele

Die folgenden Beispiele zeigen praktische Fälle, in denen mehrere Interpreter nützlich sein können.

Beispiel 1

Es gibt einen Strom von eingehenden Anfragen, die über Worker in Sub-Threads bearbeitet werden.

  • Jeder Worker-Thread hat seinen eigenen Interpreter.
  • Es gibt eine Warteschlange, um Aufgaben an die Worker zu senden, und eine weitere Warteschlange, um Ergebnisse zurückzugeben.
  • Die Ergebnisse werden in einem dedizierten Thread verarbeitet.
  • Jeder Worker läuft weiter, bis er ein „Stopp“-Sentinel (None) empfängt.
  • Der Ergebnis-Handler läuft weiter, bis alle Worker gestoppt haben.
import interpreters
from mymodule import iter_requests, handle_result

tasks = interpreters.create_queue()
results = interpreters.create_queue()

numworkers = 20
threads = []

def results_handler():
    running = numworkers
    while running:
        try:
            res = results.get(timeout=0.1)
        except interpreters.QueueEmpty:
            # No workers have finished a request since last time.
            pass
        else:
            if res is None:
                # A worker has stopped.
                running -= 1
            else:
                handle_result(res)
    empty = object()
    assert results.get_nowait(empty) is empty
threads.append(threading.Thread(target=results_handler))

def worker():
    interp = interpreters.create()
    interp.prepare_main(tasks=tasks, results=results)
    interp.exec("""if True:
        from mymodule import handle_request, capture_exception

        while True:
            req = tasks.get()
            if req is None:
                # Stop!
                break
            try:
                res = handle_request(req)
            except Exception as exc:
                res = capture_exception(exc)
            results.put(res)
        # Notify the results handler.
        results.put(None)
        """)
threads.extend(threading.Thread(target=worker) for _ in range(numworkers))

for t in threads:
    t.start()

for req in iter_requests():
    tasks.put(req)
# Send the "stop" signal.
for _ in range(numworkers):
    tasks.put(None)

for t in threads:
    t.join()

Beispiel 2

Dieser Fall ähnelt dem letzten, da es viele Worker in Sub-Threads gibt. Diesmal zerlegt der Code ein großes Array von Daten, wobei jeder Worker einen Teil nach dem anderen verarbeitet. Das Kopieren dieser Daten in jeden Interpreter wäre extrem ineffizient, daher nutzt der Code die direkte gemeinsame Nutzung von memoryview-Buffern.

  • Alle Interpreter teilen den Buffer des Quellarrays.
  • Jeder schreibt seine Ergebnisse in einen zweiten gemeinsamen Buffer.
  • Es wird eine Warteschlange verwendet, um Aufgaben an die Worker zu senden.
  • Immer nur ein Worker liest einen bestimmten Index im Quellarray.
  • Immer nur ein Worker schreibt in einen bestimmten Index in die Ergebnisse (dies ist die Art und Weise, wie Thread-Sicherheit gewährleistet wird).
import interpreters
import queue
from mymodule import read_large_data_set, use_results

numworkers = 3
data, chunksize = read_large_data_set()
buf = memoryview(data)
numchunks = (len(buf) + 1) / chunksize
results = memoryview(b'\0' * numchunks)

tasks = interpreters.create_queue()

def worker(id):
    interp = interpreters.create()
    interp.prepare_main(data=buf, results=results, tasks=tasks)
    interp.exec("""if True:
        from mymodule import reduce_chunk

        while True:
            req = tasks.get()
            if res is None:
                # Stop!
                break
            resindex, start, end = req
            chunk = data[start: end]
            res = reduce_chunk(chunk)
            results[resindex] = res
        """)
threads = [threading.Thread(target=worker) for _ in range(numworkers)]
for t in threads:
    t.start()

for i in range(numchunks):
    # Assume there's at least one worker running still.
    start = i * chunksize
    end = start + chunksize
    if end > len(buf):
        end = len(buf)
    tasks.put((start, end, i))
# Send the "stop" signal.
for _ in range(numworkers):
    tasks.put(None)

for t in threads:
    t.join()

use_results(results)

Begründung

Eine minimale API

Da das Kern-Entwicklerteam keine wirkliche Erfahrung damit hat, wie Benutzer mehrere Interpreter in Python-Code verwenden werden, hält dieser Vorschlag die anfängliche API bewusst schlank und minimal. Ziel ist es, eine gut durchdachte Grundlage zu schaffen, auf der später bei Bedarf weitere (fortgeschrittenere) Funktionalität hinzugefügt werden kann.

Dennoch berücksichtigt das vorgeschlagene Design Lektionen, die aus der bestehenden Nutzung von Subinterpretern durch die Community, aus bestehenden Standardbibliotheksmodulen und aus anderen Programmiersprachen gelernt wurden. Es berücksichtigt auch die Erfahrungen aus der Verwendung von Subinterpretern in der CPython-Testsuite und in Concurrency Benchmarks.

create(), create_queue()

Typischerweise rufen Benutzer einen Typ auf, um Instanzen des Typs zu erstellen, zu welchem Zeitpunkt die Ressourcen des Objekts provisioniert werden. Das Modul interpreters verfolgt einen anderen Ansatz, bei dem Benutzer create() aufrufen müssen, um einen neuen Interpreter zu erhalten, oder create_queue() für eine neue Warteschlange. Das direkte Aufrufen von interpreters.Interpreter() gibt nur einen Wrapper um einen bestehenden Interpreter zurück (ebenso für interpreters.Queue()).

Dies liegt daran, dass Interpreter (und Warteschlangen) spezielle Ressourcen sind. Sie existieren global im Prozess und werden nicht vom aktuellen Interpreter verwaltet/besessen. Daher macht das Modul interpreters die Erstellung eines Interpreters (oder einer Warteschlange) zu einer sichtbar unterschiedlichen Operation als die Erstellung einer Instanz von interpreters.Interpreter (oder interpreters.Queue).

Interpreter.prepare_main() setzt mehrere Variablen

prepare_main() kann als eine Art Setter-Funktion betrachtet werden. Sie unterstützt das gleichzeitige Setzen mehrerer Namen, z. B. interp.prepare_main(spam=1, eggs=2), während die meisten Setter jeweils ein Element setzen. Der Hauptgrund ist die Effizienz.

Um einen Wert im __main__.__dict__ des Interpreters zu setzen, muss die Implementierung zuerst den OS-Thread auf den identifizierten Interpreter umschalten, was einen nicht unerheblichen Overhead mit sich bringt. Nach dem Setzen des Wertes muss sie zurückschalten. Darüber hinaus gibt es einen zusätzlichen Overhead für den Mechanismus, mit dem Objekte zwischen Interpretern übergeben werden, der aggregiert reduziert werden kann, wenn mehrere Werte gleichzeitig gesetzt werden.

Daher unterstützt prepare_main() das gleichzeitige Setzen mehrerer Werte.

Weitergabe von Ausnahmen

Eine nicht abgefangene Ausnahme aus einem Subinterpreter, über Interpreter.exec(), könnte entweder (effektiv) ignoriert werden, wie es threading.Thread() tut, oder weitergegeben werden, wie es das eingebaute exec() tut. Da Interpreter.exec() eine synchrone Operation ist, wie das eingebaute exec(), werden nicht abgefangene Ausnahmen weitergegeben.

Diese Ausnahmen werden jedoch nicht direkt ausgelöst. Das liegt daran, dass Interpreter voneinander isoliert sind und keine Objekte teilen dürfen, einschließlich Ausnahmen. Dies könnte durch Auslösen eines Stellvertreters der Ausnahme addressed werden, sei es eine Zusammenfassung, eine Kopie oder ein Proxy, der sie umschließt. Jede dieser Optionen könnte den Traceback bewahren, was für die Fehlersuche nützlich ist. Die ausgelöste ExecutionFailed ist ein solcher Stellvertreter.

Es gibt noch ein weiteres Problem zu berücksichtigen. Wenn eine weitergegebene Ausnahme nicht sofort abgefangen wird, steigt sie den Aufrufstapel hoch, bis sie abgefangen (oder nicht) wird. Wenn Code irgendwo anders sie abfangen kann, ist es hilfreich zu identifizieren, dass die Ausnahme von einem Subinterpreter stammt (d. h. einer „entfernten“ Quelle) und nicht vom aktuellen Interpreter. Deshalb löst Interpreter.exec() ExecutionFailed aus und deshalb ist es eine einfache Exception und keine Kopie oder ein Proxy mit einer Klasse, die der ursprünglichen Ausnahme entspricht. Zum Beispiel würde ein nicht abgefangener ValueError aus einem Subinterpreter niemals in einem späteren try: ... except ValueError: ... abgefangen werden. Stattdessen muss ExecutionFailed direkt behandelt werden.

Im Gegensatz dazu beinhalten Ausnahmen, die von Interpreter.call() weitergegeben werden, nicht ExecutionFailed, sondern werden direkt ausgelöst, als ob sie im aufrufenden Interpreter entstanden wären. Dies liegt daran, dass Interpreter.call() eine höherrangige Methode ist, die Pickle verwendet, um Objekte zu unterstützen, die normalerweise nicht zwischen Interpretern übergeben werden können.

Objekte vs. ID-Proxies

Sowohl für Interpreter als auch für Warteschlangen verwendet das Low-Level-Modul Proxy-Objekte, die den zugrunde liegenden Zustand über ihre entsprechenden prozessglobalen IDs preisgeben. In beiden Fällen ist der Zustand ebenfalls prozessglobal und wird von mehreren Interpretern verwendet. Daher sind sie nicht geeignet, als PyObject implementiert zu werden, was nur für interpreterspezifische Daten eine Option ist. Deshalb bietet das Modul interpreters stattdessen Objekte, die schwach über die ID assoziiert sind.

Abgelehnte Ideen

Siehe PEP 554.


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

Zuletzt geändert: 2025-07-06 09:38:43 GMT