PEP 554 – Mehrere Interpreter in der Standardbibliothek
- Autor:
- Eric Snow <ericsnowcurrently at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Abgelöst
- Typ:
- Standards Track
- Erstellt:
- 05-Sep-2017
- Python-Version:
- 3.13
- Post-History:
- 07. Sep. 2017, 08. Sep. 2017, 13. Sep. 2017, 05. Dez. 2017, 04. Mai 2020, 14. Mär. 2023, 01. Nov. 2023
- Ersetzt-Durch:
- 734
Inhaltsverzeichnis
- Zusammenfassung
- Vorschlag
- Beispiele
- Isolierten Code im aktuellen Betriebssystem-Thread ausführen
- In einem anderen Thread ausführen
- Einen Interpreter vorab füllen
- Ausnahmebehandlung
- Ausnahme erneut auslösen
- Mit dem __main__-Namespace interagieren
- Synchronisation mittels einer OS-Pipe
- Einen File Descriptor teilen
- Objekte per Pickle übergeben
- Standardausgabe eines Interpreters erfassen
- Ein Modul ausführen
- Als Skript ausführen (einschließlich Zip-Archive & Verzeichnisse)
- Einen Kanal zur Kommunikation nutzen
- Ein memoryview teilen (imagine map-reduce)
- Begründung
- Über Subinterpreter
- Alternative Python-Implementierungen
- API des Moduls „interpreters“
- Interpreter-Beschränkungen
- API für die Kommunikation
- Dokumentation
- Alternative Lösungen
- Offene Fragen
- Zurückgestellte Funktionalität
- Bequemlichkeits-API hinzufügen
- Verwechslungen bezüglich der Ausführung von Interpretern im aktuellen Thread vermeiden
- „Running“ vs. „hat Threads“ klären
- Eine Dunder-Methode zum Teilen
- Interpreter.call()
- Interpreter.run_in_thread()
- Synchronisationsprimitive
- CSP-Bibliothek
- Syntaktische Unterstützung
- Multiprocessing
- C-Erweiterung Opt-in/Opt-out
- Kanäle vergiften
- __main__ zurücksetzen
- Den Zustand eines Interpreters zurücksetzen
- Den Zustand eines vorhandenen Interpreters kopieren
- Teilbare File Descriptors und Sockets
- Integration mit async
- Unterstützung für Iteration
- Kanal-Kontextmanager
- Pipes und Queues
- Ein Lock von send() zurückgeben
- Priorisierung in Kanälen unterstützen
- Unterstützung für das Erben von Einstellungen (und mehr?)
- Ausnahmen teilbar machen
- Alles durch Serialisierung teilbar machen
- RunFailedError.__cause__ verzögert machen
- Einen Wert von
interp.exec()zurückgeben - Ein teilbares Synchronisationsprimitive hinzufügen
- SystemExit und KeyboardInterrupt unterschiedlich weitergeben
- Ein explizites release() und close() zu Channel-End-Klassen hinzufügen
- SendChannel.send_buffer() hinzufügen
- Automatisch in einem Thread ausführen
- Abgelehnte Ideen
- Explizite Kanalassoziation
- Eine API basierend auf Pipes hinzufügen
- Eine API basierend auf Queues hinzufügen
- „enumerate“
- Alternative Lösungen zur Verhinderung von Ausnahmenlecks zwischen Interpretern
- Jeden neuen Interpreter immer mit seinem eigenen Thread verbinden
- Interpreter nur bei Verwendung verbinden
- Mehrere gleichzeitige Aufrufe von Interpreter.exec() zulassen
- Eine „reraise“-Methode zu RunFailedError hinzufügen
- Implementierung
- Referenzen
- Urheberrecht
Hinweis
Dieser PEP wird effektiv in einer saubereren Form in PEP 734 fortgesetzt. Dieser PEP wird aus Gründen der verschiedenen Abschnitte mit Hintergrundinformationen und zurückgestellten/abgelehnten Ideen beibehalten, die aus PEP 734 entfernt wurden.
Zusammenfassung
CPython unterstützt mehrere Interpreter im selben Prozess (auch bekannt als „Subinterpreter“) seit Version 1.5 (1997). Die Funktion ist über die C-API verfügbar. [c-api] Mehrere Interpreter operieren in relativer Isolation voneinander, was neuartige alternative Ansätze für Nebenläufigkeit ermöglicht.
Dieser Vorschlag führt das Modul interpreters der Standardbibliothek ein. Es exponiert die grundlegende Funktionalität mehrerer Interpreter, die bereits von der C-API bereitgestellt wird, zusammen mit grundlegender Unterstützung für die Kommunikation zwischen Interpretern. Dieses Modul ist besonders relevant, da PEP 684 in Python 3.12 ein pro-Interpreter GIL eingeführt hat.
Vorschlag
Zusammenfassung
- ein neues Modul der Standardbibliothek hinzufügen: „interpreters“
- concurrent.futures.InterpreterPoolExecutor hinzufügen
- Hilfe für Wartende von Erweiterungsmodulen
Das Modul „interpreters“
Das Modul interpreters wird eine High-Level-Schnittstelle zur Funktionalität mehrerer Interpreter bereitstellen und eine neue Low-Level- _interpreters (ähnlich wie das Modul threading) wrappen. Siehe den Abschnitt Beispiele für konkrete Nutzung und Anwendungsfälle.
Zusätzlich zur Bereitstellung der bestehenden (in CPython) Unterstützung für mehrere Interpreter wird das Modul auch einen grundlegenden Mechanismus für die Übergabe von Daten zwischen Interpretern unterstützen. Dies beinhaltet das Setzen von „teilbaren“ Objekten im Modul __main__ eines Ziel-Subinterpreters. Einige dieser Objekte, wie os.pipe(), können zur weiteren Kommunikation verwendet werden. Das Modul wird auch eine minimale Implementierung von „Kanälen“ als Demonstration der Cross-Interpreter-Kommunikation bereitstellen.
Beachten Sie, dass Objekte nicht zwischen Interpretern geteilt werden, da sie an den Interpreter gebunden sind, in dem sie erstellt wurden. Stattdessen werden die Daten der Objekte zwischen Interpretern übergeben. Weitere Details zum Teilen/Kommunizieren zwischen Interpretern finden Sie in den Abschnitten Gemeinsame Daten und API für die Kommunikation.
API-Zusammenfassung für das Modul interpreters
Hier ist eine Zusammenfassung der API für das Modul interpreters. Eine detailliertere Erklärung der vorgeschlagenen Klassen und Funktionen finden Sie im Abschnitt „interpreters“ Modul API unten.
Zum Erstellen und Verwenden von Interpretern
| Signatur | description |
|---|---|
list_all() -> [Interpreter] |
Alle vorhandenen Interpreter abrufen. |
get_current() -> Interpreter |
Den aktuell laufenden Interpreter abrufen. |
get_main() -> Interpreter |
Den Hauptinterpreter abrufen. |
create() -> Interpreter |
Einen neuen (inaktiven) Python-Interpreter initialisieren. |
| Signatur | description |
|---|---|
class Interpreter |
Ein einzelner Interpreter. |
.id |
Die ID des Interpreters (nur lesbar). |
.is_running() -> bool |
Führt der Interpreter gerade Code aus? |
.close() |
Den Interpreter finalisieren und zerstören. |
.set_main_attrs(**kwargs) |
„Teilbare“ Objekte in __main__ binden. |
.get_main_attr(name) |
Ein „teilbares“ Objekt aus __main__ abrufen. |
.exec(src_str, /) |
Den gegebenen Quellcode im Interpreter ausführen
(im aktuellen Thread).
|
Für die Kommunikation zwischen Interpretern
| Signatur | description |
|---|---|
is_shareable(obj) -> Bool |
Können die Daten des Objekts
zwischen Interpretern übergeben werden?
|
create_channel() -> (RecvChannel, SendChannel) |
Einen neuen Kanal zum Übergeben
von Daten zwischen Interpretern erstellen.
|
concurrent.futures.InterpreterPoolExecutor
Ein Executor wird hinzugefügt, der ThreadPoolExecutor erweitert, um Thread-Aufgaben in Subinterpretern auszuführen. Anfangs werden die einzigen unterstützten Aufgaben das sein, was Interpreter.exec() akzeptiert (z. B. ein Skript als str). Wir könnten jedoch auch einige Funktionen unterstützen, sowie schließlich eine separate Methode zum Picklen der Aufgabe und der Argumente, um Reibungsverluste zu reduzieren (auf Kosten der Leistung für kurzlaufende Aufgaben).
Hilfe für Wartende von Erweiterungsmodulen
In der Praxis wird ein Erweiterungsmodul, das eine Multi-Phase-Initialisierung implementiert (PEP 489), als isoliert betrachtet und ist somit mit mehreren Interpretern kompatibel. Andernfalls ist es „inkompatibel“.
Viele Erweiterungsmodule sind immer noch inkompatibel. Die Wartenden und Benutzer solcher Erweiterungsmodule werden beide profitieren, wenn sie aktualisiert werden, um mehrere Interpreter zu unterstützen. Bis dahin können sich Benutzer durch Fehler bei der Verwendung mehrerer Interpreter verwirrt fühlen, was sich negativ auf die Wartenden von Erweiterungen auswirken könnte. Siehe Bedenken unten.
Um diese Auswirkungen abzumildern und die Kompatibilität zu beschleunigen, werden wir Folgendes tun:
- klarstellen, dass Erweiterungsmodule nicht verpflichtet sind, die Verwendung in mehreren Interpretern zu unterstützen
- einen
ImportErrorauslösen, wenn ein inkompatibles Modul in einem Subinterpreter importiert wird - Ressourcen bereitstellen (z. B. Dokumente), um Wartende bei der Erreichung der Kompatibilität zu unterstützen
- die Wartenden von Cython und den am häufigsten verwendeten Erweiterungsmodulen (auf PyPI) kontaktieren, um Feedback zu erhalten und möglicherweise Unterstützung anzubieten
Beispiele
Isolierten Code im aktuellen Betriebssystem-Thread ausführen
interp = interpreters.create()
print('before')
interp.exec('print("during")')
print('after')
In einem anderen Thread ausführen
interp = interpreters.create()
def run():
interp.exec('print("during")')
t = threading.Thread(target=run)
print('before')
t.start()
t.join()
print('after')
Einen Interpreter vorab füllen
interp = interpreters.create()
interp.exec(tw.dedent("""
import some_lib
import an_expensive_module
some_lib.set_up()
"""))
wait_for_request()
interp.exec(tw.dedent("""
some_lib.handle_request()
"""))
Ausnahmebehandlung
interp = interpreters.create()
try:
interp.exec(tw.dedent("""
raise KeyError
"""))
except interpreters.RunFailedError as exc:
print(f"got the error from the subinterpreter: {exc}")
Ausnahme erneut auslösen
interp = interpreters.create()
try:
try:
interp.exec(tw.dedent("""
raise KeyError
"""))
except interpreters.RunFailedError as exc:
raise exc.__cause__
except KeyError:
print("got a KeyError from the subinterpreter")
Beachten Sie, dass dieses Muster ein Kandidat für spätere Verbesserungen ist.
Mit dem __main__-Namespace interagieren
interp = interpreters.create()
interp.set_main_attrs(a=1, b=2)
interp.exec(tw.dedent("""
res = do_something(a, b)
"""))
res = interp.get_main_attr('res')
Synchronisation mittels einer OS-Pipe
interp = interpreters.create()
r1, s1 = os.pipe()
r2, s2 = os.pipe()
def task():
interp.exec(tw.dedent(f"""
import os
os.read({r1}, 1)
print('during B')
os.write({s2}, '')
"""))
t = threading.thread(target=task)
t.start()
print('before')
os.write(s1, '')
print('during A')
os.read(r2, 1)
print('after')
t.join()
Objekte per Pickle übergeben
interp = interpreters.create()
r, s = os.pipe()
interp.exec(tw.dedent(f"""
import os
import pickle
reader = {r}
"""))
interp.exec(tw.dedent("""
data = b''
c = os.read(reader, 1)
while c != b'\x00':
while c != b'\x00':
data += c
c = os.read(reader, 1)
obj = pickle.loads(data)
do_something(obj)
c = os.read(reader, 1)
"""))
for obj in input:
data = pickle.dumps(obj)
os.write(s, data)
os.write(s, b'\x00')
os.write(s, b'\x00')
Standardausgabe eines Interpreters erfassen
interp = interpreters.create()
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
interp.exec(tw.dedent("""
print('spam!')
"""))
assert(stdout.getvalue() == 'spam!')
# alternately:
interp.exec(tw.dedent("""
import contextlib, io
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
print('spam!')
captured = stdout.getvalue()
"""))
captured = interp.get_main_attr('captured')
assert(captured == 'spam!')
Eine Pipe (os.pipe()) könnte ähnlich verwendet werden.
Ein Modul ausführen
interp = interpreters.create()
main_module = mod_name
interp.exec(f'import runpy; runpy.run_module({main_module!r})')
Als Skript ausführen (einschließlich Zip-Archive & Verzeichnisse)
interp = interpreters.create()
main_script = path_name
interp.exec(f"import runpy; runpy.run_path({main_script!r})")
Einen Kanal zur Kommunikation nutzen
tasks_recv, tasks = interpreters.create_channel()
results, results_send = interpreters.create_channel()
def worker():
interp = interpreters.create()
interp.set_main_attrs(tasks=tasks_recv, results=results_send)
interp.exec(tw.dedent("""
def handle_request(req):
...
def capture_exception(exc):
...
while True:
try:
req = tasks.recv()
except Exception:
# channel closed
break
try:
res = handle_request(req)
except Exception as exc:
res = capture_exception(exc)
results.send_nowait(res)
"""))
threads = [threading.Thread(target=worker) for _ in range(20)]
for t in threads:
t.start()
requests = ...
for req in requests:
tasks.send(req)
tasks.close()
for t in threads:
t.join()
Begründung
Das Ausführen von Code in mehreren Interpretern bietet eine nützliche Isolationsebene innerhalb desselben Prozesses. Dies kann auf verschiedene Weise genutzt werden. Darüber hinaus bieten Subinterpreter einen klar definierten Rahmen, in dem eine solche Isolation erweitert werden kann. (Siehe PEP 684.)
Alyssa (Nick) Coghlan erklärte einige der Vorteile durch einen Vergleich mit Multiprocessing [benefits]
[I] expect that communicating between subinterpreters is going
to end up looking an awful lot like communicating between
subprocesses via shared memory.
The trade-off between the two models will then be that one still
just looks like a single process from the point of view of the
outside world, and hence doesn't place any extra demands on the
underlying OS beyond those required to run CPython with a single
interpreter, while the other gives much stricter isolation
(including isolating C globals in extension modules), but also
demands much more from the OS when it comes to its IPC
capabilities.
The security risk profiles of the two approaches will also be quite
different, since using subinterpreters won't require deliberately
poking holes in the process isolation that operating systems give
you by default.
CPython unterstützt mehrere Interpreter mit zunehmendem Unterstützungsgrad seit Version 1.5. Während die Funktion das Potenzial hat, ein mächtiges Werkzeug zu sein, hat sie unter Vernachlässigung gelitten, da die Fähigkeiten mehrerer Interpreter nicht direkt aus Python zugänglich sind. Die Bereitstellung der bestehenden Funktionalität in der Standardbibliothek wird helfen, die Situation umzukehren.
Dieser Vorschlag konzentriert sich auf die Ermöglichung der grundlegenden Fähigkeit mehrerer Interpreter, voneinander isoliert, im selben Python-Prozess. Dies ist ein neues Gebiet für Python, daher gibt es relative Unsicherheit über die besten Werkzeuge, die als Begleiter zu Interpretern bereitgestellt werden sollen. Daher minimieren wir die Funktionalität, die wir im Vorschlag so weit wie möglich hinzufügen.
Bedenken
- „Subinterpreter sind die Mühe nicht wert“
Einige argumentierten, dass Subinterpreter keinen ausreichenden Nutzen bieten, um sie zu einem offiziellen Teil von Python zu machen. Das Hinzufügen von Funktionen zur Sprache (oder Standardbibliothek) hat die Kosten, die Größe der Sprache zu erhöhen. Eine Ergänzung muss sich also selbst amortisieren.
In diesem Fall bietet die Unterstützung mehrerer Interpreter ein neuartiges Nebenläufigkeitsmodell, das auf isolierte Ausführungsthreads fokussiert ist. Darüber hinaus bieten sie eine Gelegenheit für Änderungen in CPython, die die gleichzeitige Nutzung mehrerer CPU-Kerne ermöglichen (derzeit durch das GIL verhindert – siehe PEP 684).
Alternativen zu Subinterpretern sind Threading, Async und Multiprocessing. Threading wird durch das GIL begrenzt und Async ist nicht die richtige Lösung für jedes Problem (noch für jede Person). Multiprocessing ist ebenfalls in einigen, aber nicht allen Situationen wertvoll. Direkte IPC (anstatt über das multiprocessing-Modul) bietet ähnliche Vorteile, jedoch mit demselben Vorbehalt.
Bemerkenswerterweise sind Subinterpreter kein Ersatz für die oben genannten. Sicherlich überschneiden sie sich in einigen Bereichen, aber die Vorteile von Subinterpretern umfassen Isolation und (potenziell) Leistung. Insbesondere bieten Subinterpreter einen direkten Weg zu einem alternativen Nebenläufigkeitsmodell (z. B. CSP), das anderswo erfolgreich war und einige Python-Benutzer ansprechen wird. Das ist der Kernwert, den das Modul interpreters bereitstellen wird.
- „Standardbibliotheksunterstützung für mehrere Interpreter belastet C-Erweiterungsautoren zusätzlich“
Im Abschnitt Interpreter-Isolation unten identifizieren wir Wege, auf denen die Isolation in CPythons Subinterpretern unvollständig ist. Am bemerkenswertesten sind Erweiterungsmodule, die C-Globale zur Speicherung interner Zustände verwenden. (PEP 3121 und PEP 489 bieten eine Lösung für dieses Problem, gefolgt von einigen zusätzlichen APIs, die die Effizienz verbessern, z. B. PEP 573).
Folglich könnten Projekte, die Erweiterungsmodule veröffentlichen, mit einer erhöhten Wartungslast konfrontiert sein, wenn ihre Benutzer Subinterpreter zu verwenden beginnen, wo ihre Module möglicherweise fehlschlagen. Diese Situation ist auf Module beschränkt, die C-Globale verwenden (oder Bibliotheken verwenden, die C-Globale verwenden) zur Speicherung interner Zustände. Für Numpy beträgt die gemeldete Fehlerquote einen alle 6 Monate. [bug-rate]
Letztendlich kommt es auf die Frage an, wie oft es in der Praxis ein Problem sein wird: wie viele Projekte betroffen sein werden, wie oft ihre Benutzer betroffen sein werden, wie hoch die zusätzliche Wartungslast für Projekte sein wird und wie hoch der Gesamtnutzen von Subinterpretern ist, um diese Kosten auszugleichen. Die Position dieses PEP ist, dass die tatsächliche zusätzliche Wartungslast gering sein und weit unter der Schwelle liegen wird, bei der Subinterpreter sich lohnen.
- „Das Erstellen einer neuen Nebenläufigkeits-API erfordert viel mehr Gedanken und Experimente, daher sollte das neue Modul nicht sofort in die Standardbibliothek aufgenommen werden, wenn überhaupt.“
Die Einführung einer API für ein neues Nebenläufigkeitsmodell, wie es bei asyncio der Fall war, ist ein extrem großes Projekt, das viel sorgfältige Überlegung erfordert. Es ist nichts, was so einfach getan werden kann, wie dieser PEP vorschlägt, und verdient wahrscheinlich viel Zeit auf PyPI, um sich zu entwickeln. (Siehe Nathaniels Beitrag auf python-dev.)
Dieser PEP schlägt jedoch keine neue Nebenläufigkeits-API vor. Höchstens exponiert er minimale Werkzeuge (z. B. Subinterpreter, Kanäle), die verwendet werden können, um Code zu schreiben, der Mustern folgt, die mit (relativ) neuen Nebenläufigkeitsmodellen für Python verbunden sind Nebenläufigkeitsmodelle. Diese Werkzeuge könnten auch als Grundlage für APIs für solche Nebenläufigkeitsmodelle verwendet werden. Auch hier schlägt dieser PEP keine solche API vor.
- „Es hat keinen Sinn, Subinterpreter zu exponieren, wenn sie immer noch das GIL teilen.“
- „Die Anstrengung, das GIL pro Interpreter zu machen, ist störend und riskant.“
Ein häufiges Missverständnis ist, dass dieser PEP auch ein Versprechen enthält, dass Interpreter das GIL nicht mehr teilen werden. Wenn das geklärt ist, ist die nächste Frage: „Was ist der Sinn?“ Diese wird bereits ausführlich in diesem PEP beantwortet. Nur zur Klarstellung: Der Wert liegt in
* increase exposure of the existing feature, which helps improve
the code health of the entire CPython runtime
* expose the (mostly) isolated execution of interpreters
* preparation for per-interpreter GIL
* encourage experimentation
- „Datenaustausch kann sich negativ auf die Cache-Leistung in Multi-Core-Szenarien auswirken.“
(Siehe [cache-line-ping-pong].)
Das sollte im Moment kein Problem sein, da wir keine unmittelbaren Pläne haben, Daten zwischen Interpretern tatsächlich zu teilen, sondern uns auf das Kopieren konzentrieren.
Über Subinterpreter
Nebenläufigkeit
Nebenläufigkeit ist ein anspruchsvolles Gebiet der Softwareentwicklung. Jahrzehnte der Forschung und Praxis haben zu einer breiten Vielfalt von Nebenläufigkeitsmodellen geführt, jedes mit unterschiedlichen Zielen. Die meisten konzentrieren sich auf Korrektheit und Benutzerfreundlichkeit.
Eine Klasse von Nebenläufigkeitsmodellen konzentriert sich auf isolierte Ausführungsthreads, die über ein gewisses Nachrichtenweiterleitungsschema interoperieren. Ein bemerkenswertes Beispiel sind Communicating Sequential Processes [CSP] (auf dem Go's Nebenläufigkeit grob basiert). Die inhärente Isolation von CPythons Interpretern macht sie gut geeignet für diesen Ansatz.
Interpreter-Isolation
CPyhtons Interpreter sollen streng voneinander isoliert sein. Jeder Interpreter hat seine eigene Kopie aller Module, Klassen, Funktionen und Variablen. Das Gleiche gilt für den Zustand in C, einschließlich Erweiterungsmodulen. Die CPython C-API-Dokumente erklären mehr. [caveats]
Es gibt jedoch Möglichkeiten, wie Interpreter einige Zustände teilen. Erstens bleibt ein Teil des prozessglobalen Zustands gemeinsam genutzt
- Dateideskriptoren
- Low-Level-Umgebungsvariablen
- Prozessspeicher (obwohl Allocatoren isoliert sind)
- eingebaute Typen (z. B. dict, bytes)
- Singletons (z. B. None)
- zugrunde liegende statische Moduldaten (z. B. Funktionen) für eingebaute/Erweiterungs-/eingefrorene Module
Es gibt keine Pläne, dies zu ändern.
Zweitens ist die Isolation aufgrund von Fehlern oder Implementierungen, die Subinterpreter nicht berücksichtigten, fehlerhaft. Dazu gehören Dinge wie Erweiterungsmodule, die auf C-Globale angewiesen sind. [cryptography] In diesen Fällen sollten Fehler gemeldet werden (einige sind bereits gemeldet)
- Readline-Modul-Hook-Funktionen (http://bugs.python.org/issue4202)
- Speicherlecks bei Re-Init (http://bugs.python.org/issue21387)
Schließlich fehlt aufgrund des aktuellen Designs von CPython eine gewisse potenzielle Isolation. Verbesserungen werden derzeit vorgenommen, um Lücken in diesem Bereich zu schließen
- Erweiterungen, die die
PyGILState_*API verwenden, sind einigermaßen inkompatibel [gilstate]
Bestehende Nutzung
Die Unterstützung mehrerer Interpreter war keine weit verbreitete Funktion. Tatsächlich gab es nur eine Handvoll dokumentierter Fälle weit verbreiteter Nutzung, darunter mod_wsgi, OpenStack Ceph und JEP. Einerseits geben diese Fälle Vertrauen, dass die bestehende Unterstützung für mehrere Interpreter relativ stabil ist. Andererseits gibt es keine große Stichprobengröße, um den Nutzen der Funktion zu beurteilen.
Alternative Python-Implementierungen
Ich habe Feedback von verschiedenen Python-Implementierern zur Unterstützung von Subinterpretern eingeholt. Jeder hat angegeben, dass er mehrere Interpreter im selben Prozess (wenn er sich dafür entscheidet) ohne große Schwierigkeiten unterstützen könnte. Hier sind die von mir kontaktierten Projekte:
- jython ([jython])
- ironpython (persönliche Korrespondenz)
- pypy (persönliche Korrespondenz)
- micropython (persönliche Korrespondenz)
API des Moduls „interpreters“
Das Modul bietet die folgenden Funktionen:
list_all() -> [Interpreter]
Return a list of all existing interpreters.
get_current() => Interpreter
Return the currently running interpreter.
get_main() => Interpreter
Return the main interpreter. If the Python implementation
has no concept of a main interpreter then return None.
create() -> Interpreter
Initialize a new Python interpreter and return it.
It will remain idle until something is run in it and always
run in its own thread.
is_shareable(obj) -> bool:
Return True if the object may be "shared" between interpreters.
This does not necessarily mean that the actual objects will be
shared. Instead, it means that the objects' underlying data will
be shared in a cross-interpreter way, whether via a proxy, a
copy, or some other means.
Das Modul bietet auch die folgende Klasse:
class Interpreter(id):
id -> int:
The interpreter's ID. (read-only)
is_running() -> bool:
Return whether or not the interpreter's "exec()" is currently
executing code. Code running in subthreads is ignored.
Calling this on the current interpreter will always return True.
close():
Finalize and destroy the interpreter.
This may not be called on an already running interpreter.
Doing so results in a RuntimeError.
set_main_attrs(iterable_or_mapping, /):
set_main_attrs(**kwargs):
Set attributes in the interpreter's __main__ module
corresponding to the given name-value pairs. Each value
must be a "shareable" object and will be converted to a new
object (e.g. copy, proxy) in whatever way that object's type
defines. If an attribute with the same name is already set,
it will be overwritten.
This method is helpful for setting up an interpreter before
calling exec().
get_main_attr(name, default=None, /):
Return the value of the corresponding attribute of the
interpreter's __main__ module. If the attribute isn't set
then the default is returned. If it is set, but the value
isn't "shareable" then a ValueError is raised.
This may be used to introspect the __main__ module, as well
as a very basic mechanism for "returning" one or more results
from Interpreter.exec().
exec(source_str, /):
Run the provided Python source code in the interpreter,
in its __main__ module.
This may not be called on an already running interpreter.
Doing so results in a RuntimeError.
An "interp.exec()" call is similar to a builtin exec() call
(or to calling a function that returns None). Once
"interp.exec()" completes, the code that called "exec()"
continues executing (in the original interpreter). Likewise,
if there is any uncaught exception then it effectively
(see below) propagates into the code where ``interp.exec()``
was called. Like exec() (and threads), but unlike function
calls, there is no return value. If any "return" value from
the code is needed, send the data out via a pipe (os.pipe())
or channel or other cross-interpreter communication mechanism.
The big difference from exec() or functions is that
"interp.exec()" executes the code in an entirely different
interpreter, with entirely separate state. The interpreters
are completely isolated from each other, so the state of the
original interpreter (including the code it was executing in
the current OS thread) does not affect the state of the target
interpreter (the one that will execute the code). Likewise,
the target does not affect the original, nor any of its other
threads.
Instead, the state of the original interpreter (for this thread)
is frozen, and the code it's executing code completely blocks.
At that point, the target interpreter is given control of the
OS thread. Then, when it finishes executing, the original
interpreter gets control back and continues executing.
So calling "interp.exec()" will effectively cause the current
Python thread to completely pause. Sometimes you won't want
that pause, in which case you should make the "exec()" call in
another thread. To do so, add a function that calls
"interp.exec()" and then run that function in a normal
"threading.Thread".
Note that the interpreter's state is never reset, neither
before "interp.exec()" executes the code nor after. Thus the
interpreter state is preserved between calls to
"interp.exec()". This includes "sys.modules", the "builtins"
module, and the internal state of C extension modules.
Also note that "interp.exec()" executes in the namespace of the
"__main__" module, just like scripts, the REPL, "-m", and
"-c". Just as the interpreter's state is not ever reset, the
"__main__" module is never reset. You can imagine
concatenating the code from each "interp.exec()" call into one
long script. This is the same as how the REPL operates.
Supported code: source text.
Zusätzlich zur Funktionalität von Interpreter.set_main_attrs() bietet das Modul einen verwandten Weg zur Übergabe von Daten zwischen Interpretern: Kanäle. Siehe Kanäle unten.
Nicht abgefangene Ausnahmen
Bezüglich nicht abgefangener Ausnahmen in Interpreter.exec() stellten wir fest, dass sie „effektiv“ in den Code propagiert werden, in dem interp.exec() aufgerufen wurde. Um Ausnahmen (und Tracebacks) zwischen Interpretern nicht durchsickern zu lassen, erstellen wir eine Ersatzdarstellung der Ausnahme und ihres Tracebacks (siehe traceback.TracebackException), setzen sie auf __cause__ einer neuen interpreters.RunFailedError und lösen diese aus.
Das direkte Auslösen (eines Proxys der) Ausnahme ist problematisch, da es schwieriger ist, zwischen einem Fehler im interp.exec()-Aufruf und einer nicht abgefangenen Ausnahme aus dem Subinterpreter zu unterscheiden.
Interpreter-Beschränkungen
Jeder neue Interpreter, der von interpreters.create() erstellt wird, unterliegt nun spezifischen Einschränkungen für den ausgeführten Code. Dazu gehören die folgenden:
- Das Importieren eines Erweiterungsmoduls schlägt fehl, wenn es keine Multi-Phase-Initialisierung implementiert.
- Daemon-Threads dürfen nicht erstellt werden.
os.fork()ist nicht erlaubt (also keinmultiprocessing).os.exec*()ist nicht erlaubt (aber „fork+exec“, wie beisubprocess, ist in Ordnung).
Beachten Sie, dass Interpreter, die mit der vorhandenen C-API erstellt wurden, diese Einschränkungen nicht haben. Das Gleiche gilt für den „Hauptinterpreter“, sodass sich die bestehende Python-Nutzung nicht ändert.
Möglicherweise entscheiden wir uns später dafür, einige der oben genannten Einschränkungen zu lockern oder eine Möglichkeit zur individuellen Aktivierung/Deaktivierung granularer Einschränkungen bereitzustellen. Unabhängig davon wird die Anforderung der Multi-Phase-Initialisierung von Erweiterungsmodulen immer eine Standardbeschränkung sein.
API für die Kommunikation
Wie in Gemeinsame Daten oben diskutiert, ist die Unterstützung mehrerer Interpreter ohne einen Mechanismus zum Teilen von Daten (Kommunizieren) zwischen ihnen weniger nützlich. Das Teilen tatsächlicher Python-Objekte zwischen Interpretern birgt jedoch so viele potenzielle Probleme, dass wir die Unterstützung dafür in diesem Vorschlag vermeiden. Ebenso fügen wir, wie bereits erwähnt, nicht mehr als einen grundlegenden Kommunikationsmechanismus hinzu.
Dieser Mechanismus ist die Methode Interpreter.set_main_attrs(). Sie kann verwendet werden, um globale Variablen einzurichten, bevor Interpreter.exec() aufgerufen wird. Die an set_main_attrs() übergebenen Namens-Wert-Paare werden als Attribute des __main__-Moduls des Interpreters gebunden. Die Werte müssen „teilbar“ sein. Siehe Teilbare Typen unten.
Zusätzliche Ansätze zur Kommunikation und zum Teilen von Objekten werden durch Interpreter.set_main_attrs() ermöglicht. Ein teilbares Objekt könnte implementiert werden, das wie eine Warteschlange funktioniert, aber mit Cross-Interpreter-Sicherheit. Tatsächlich enthält dieser PEP ein Beispiel für einen solchen Ansatz: Kanäle.
Kanäle
Das Modul interpreters wird eine dedizierte Lösung für die Übergabe von Objektdaten zwischen Interpretern enthalten: Kanäle. Sie sind teilweise im Modul enthalten, um einen einfacheren Mechanismus als die Verwendung von os.pipe() bereitzustellen und teilweise, um zu demonstrieren, wie Bibliotheken Interpreter.set_main_attrs() und das von ihr verwendete Protokoll nutzen können.
Ein Kanal ist ein Simplex-FIFO. Es ist ein grundlegender, optionaler Mechanismus zum Teilen von Daten, der von Pipes, Queues und CSP-Kanälen inspiriert ist. [fifo] Der Hauptunterschied zu Pipes besteht darin, dass Kanäle mit null oder mehr Interpretern auf beiden Seiten verbunden werden können. Wie Queues, die ebenfalls Many-to-Many sind, sind Kanäle gepuffert (obwohl sie auch Methoden mit unbuffered Semantik anbieten).
Kanäle haben zwei Operationen: Senden und Empfangen. Ein Schlüsselmerkmal dieser Operationen ist, dass Kanäle Daten übertragen, die von Python-Objekten abgeleitet sind, und nicht die Objekte selbst. Wenn Objekte gesendet werden, werden ihre Daten extrahiert. Wenn der "Objekt" im anderen Interpreter empfangen wird, werden die Daten zurück in ein Objekt konvertiert, das diesem Interpreter gehört.
Damit dies funktioniert, wird der mutable gemeinsame Zustand vom Python-Runtime verwaltet, nicht von einem der Interpreter. Anfangs werden wir nur einen Objekttyp für den gemeinsamen Zustand unterstützen: die von interpreters.create_channel() bereitgestellten Kanäle. Kanäle wiederum werden die Übergabe von Objekten zwischen Interpretern sorgfältig verwalten.
Dieser Ansatz, einschließlich der Beibehaltung einer minimalen API, hilft uns, weitere Komplexität vor Python-Benutzern zu verbergen.
Das Modul interpreters bietet die folgende Funktion in Bezug auf Kanäle.
create_channel() -> (RecvChannel, SendChannel):
Create a new channel and return (recv, send), the RecvChannel
and SendChannel corresponding to the ends of the channel.
Both ends of the channel are supported "shared" objects (i.e.
may be safely shared by different interpreters. Thus they
may be set using "Interpreter.set_main_attrs()".
Das Modul bietet auch die folgenden klassenbezogenen Klassen.
class RecvChannel(id):
The receiving end of a channel. An interpreter may use this to
receive objects from another interpreter. Any type supported by
Interpreter.set_main_attrs() will be supported here, though at
first only a few of the simple, immutable builtin types
will be supported.
id -> int:
The channel's unique ID. The "send" end has the same one.
recv(*, timeout=None):
Return the next object from the channel. If none have been
sent then wait until the next send (or until the timeout is hit).
At the least, the object will be equivalent to the sent object.
That will almost always mean the same type with the same data,
though it could also be a compatible proxy. Regardless, it may
use a copy of that data or actually share the data. That's up
to the object's type.
recv_nowait(default=None):
Return the next object from the channel. If none have been
sent then return the default. Otherwise, this is the same
as the "recv()" method.
class SendChannel(id):
The sending end of a channel. An interpreter may use this to
send objects to another interpreter. Any type supported by
Interpreter.set_main_attrs() will be supported here, though
at first only a few of the simple, immutable builtin types
will be supported.
id -> int:
The channel's unique ID. The "recv" end has the same one.
send(obj, *, timeout=None):
Send the object (i.e. its data) to the "recv" end of the
channel. Wait until the object is received. If the object
is not shareable then ValueError is raised.
The builtin memoryview is supported, so sending a buffer
across involves first wrapping the object in a memoryview
and then sending that.
send_nowait(obj):
Send the object to the "recv" end of the channel. This
behaves the same as "send()", except for the waiting part.
If no interpreter is currently receiving (waiting on the
other end) then queue the object and return False. Otherwise
return True.
Dokumentation
Die neue Dokumentationsseite der Standardbibliothek für das Modul interpreters wird Folgendes enthalten:
- (oben) eine klare Anmerkung, dass die Unterstützung für mehrere Interpreter von Erweiterungsmodulen nicht verlangt wird.
- einige Erklärungen, was Subinterpreten sind.
- kurze Beispiele für die Verwendung mehrerer Interpreter (und die Kommunikation zwischen ihnen).
- eine Zusammenfassung der Einschränkungen bei der Verwendung mehrerer Interpreter.
- (für Maintainer von Erweiterungen) ein Link zu den Ressourcen für die Sicherstellung der Kompatibilität mit mehreren Interpretern.
- viel der API-Informationen in diesem PEP.
Dokumentationen zu Ressourcen für Maintainer von Erweiterungen existieren bereits auf der Howto-Seite "Isolating Extension Modules". Zusätzliche Hilfe wird dort hinzugefügt. Es kann beispielsweise hilfreich sein, Strategien für den Umgang mit verknüpften Bibliotheken zu diskutieren, die ihren eigenen, mit Subinterpretern inkompatiblen globalen Zustand beibehalten.
Beachten Sie, dass die Dokumentation einen großen Teil zur Abmilderung negativer Auswirkungen beitragen wird, die das neue Modul interpreters auf Maintainer von Erweiterungsmodulen haben könnte.
Außerdem wird der ImportError für inkompatible Erweiterungsmodule aktualisiert, um klarzustellen, dass er aufgrund fehlender Kompatibilität mit mehreren Interpretern auftritt und dass Erweiterungen dies nicht anbieten müssen. Dies wird helfen, die Erwartungen der Benutzer richtig zu setzen.
Alternative Lösungen
Eine mögliche Alternative zu einem neuen Modul ist die Hinzufügung von Unterstützung für Interpreter zu concurrent.futures. Es gibt mehrere Gründe, warum das nicht funktionieren würde:
- Der offensichtliche Ort für die Suche nach Unterstützung für mehrere Interpreter ist ein "interpreters"-Modul, ähnlich wie bei "threading" usw.
concurrent.futuresdreht sich alles um die Ausführung von Funktionen, aber derzeit haben wir keine gute Möglichkeit, eine Funktion von einem Interpreter in einem anderen auszuführen.
Ähnliche Überlegungen gelten für die Unterstützung im Modul multiprocessing.
Offene Fragen
- Wird es zu verwirrend sein, dass
interp.exec()im aktuellen Thread läuft? - Sollten wir jetzt Fallbacks für Pickling für
interp.exec()und/oderInterpreter.set_main_attrs()undInterpreter.get_main_attr()hinzufügen? - Sollten wir (begrenzte) Funktionen in
interp.exec()jetzt unterstützen? - umbenennen von
Interpreter.close()zuInterpreter.destroy()? - fallen lassen von
Interpreter.get_main_attr(), da wir Kanäle haben? - sollten Kanäle ein eigener PEP sein?
Zurückgestellte Funktionalität
Um diesen Vorschlag minimalistisch zu halten, wurde die folgende Funktionalität für zukünftige Überlegungen zurückgestellt. Beachten Sie, dass dies keine Bewertung der genannten Funktionen ist, sondern eine Verschiebung. Jede einzelne ist wohlbegründet.
Bequemlichkeits-API hinzufügen
Es gibt eine Reihe von Dingen, die hypothetische Probleme mit dem neuen Modul glätten könnten.
- etwas wie
Interpreter.run()oderInterpreter.call()hinzufügen, dasinterp.exec()aufruft und auf Pickle zurückfällt. - auf Pickle in
Interpreter.set_main_attrs()undInterpreter.get_main_attr()zurückfallen.
Dies wäre einfach zu tun, wenn sich dies als Schwachstelle erweist.
Verwechslungen bezüglich der Ausführung von Interpretern im aktuellen Thread vermeiden
Ein häufiger Verwirrungspunkt war, dass Interpreter.exec() im aktuellen Betriebssystem-Thread ausgeführt wird und den aktuellen Python-Thread vorübergehend blockiert. Es könnte sich lohnen, etwas zu tun, um diese Verwirrung zu vermeiden.
Einige mögliche Lösungen für dieses hypothetische Problem:
- Standardmäßig in einem neuen Thread ausführen?
- hinzufügen von
Interpreter.exec_in_thread()? - hinzufügen von
Interpreter.exec_in_current_thread()?
In früheren Versionen dieses PEP war die Methode interp.run(). Die einfache Änderung zu interp.exec() allein wird wahrscheinlich die Verwirrung ausreichend reduzieren, wenn sie mit der Aufklärung der Benutzer durch die Dokumentation kombiniert wird. Wenn es sich als echtes Problem herausstellt, können wir zu diesem Zeitpunkt eine der Alternativen verfolgen.
„Running“ vs. „hat Threads“ klären
Interpreter.is_running() bezieht sich speziell darauf, ob Interpreter.exec() (oder ähnliches) irgendwo läuft. Sie sagt nichts darüber aus, ob der Interpreter Sub-Threads laufen hat. Diese Information könnte hilfreich sein.
Einige Dinge, die wir tun könnten:
- umbenennen von
Interpreter.is_running()zuInterpreter.is_running_main() - hinzufügen von
Interpreter.has_threads(), umInterpreter.is_running()zu ergänzen. - erweitern auf
Interpreter.is_running(main=True, threads=False)
Keines davon ist dringend und jedes könnte später, wenn gewünscht, durchgeführt werden.
Eine Dunder-Methode zum Teilen
Wir könnten eine spezielle Methode wie __xid__ hinzufügen, die tp_xid entspricht. Zumindest würde sie es Python-Typen ermöglichen, ihre Instanzen in einen anderen Typ zu konvertieren, der tp_xid implementiert.
Das Problem ist, dass die Exposition dieser Funktionalität gegenüber Python-Code ein gewisses Maß an Komplexität mit sich bringt, das noch nicht erforscht wurde, und es gibt keinen überzeugenden Grund, diese Komplexität zu untersuchen.
Interpreter.call()
Es wäre praktisch, bestehende Funktionen direkt in Subinterpretern auszuführen. Interpreter.exec() könnte angepasst werden, um dies zu unterstützen, oder eine Methode call() könnte hinzugefügt werden.
Interpreter.call(f, *args, **kwargs)
Dies leidet unter demselben Problem wie das Teilen von Objekten zwischen Interpretern über Queues. Die minimale Lösung (Ausführen eines Quelltextes) reicht aus, damit wir die Funktion herausbringen können, wo sie erforscht werden kann.
Interpreter.run_in_thread()
Diese Methode würde einen interp.exec()-Aufruf für Sie in einem Thread ausführen. Dies nur mit threading.Thread und interp.exec() zu tun, ist relativ trivial, daher haben wir es weggelassen.
Synchronisationsprimitive
Das Modul threading bietet eine Reihe von Synchronisationsprimitiven zur Koordinierung gleichzeitiger Operationen. Dies ist besonders notwendig aufgrund der gemeinsamen Zustandsnatur des Threadings. Im Gegensatz dazu teilen Interpreter keine Zustände. Datenteilung ist auf die Fähigkeit der Laufzeit zur Bereitstellung von teilbaren Objekten beschränkt, was die Notwendigkeit expliziter Synchronisation entfällt. Wenn in Zukunft eine Art optionaler Unterstützung für gemeinsam genutzte Zustände zu CPythons Interpretern hinzugefügt wird, kann dieselbe Anstrengung Synchronisationsprimitive einführen, um diesem Bedarf gerecht zu werden.
CSP-Bibliothek
Ein Modul csp wäre kein großer Schritt weg von der Funktionalität, die dieser PEP bietet. Das Hinzufügen eines solchen Moduls liegt jedoch außerhalb der minimalistischen Ziele dieses Vorschlags.
Syntaktische Unterstützung
Die Sprache Go bietet ein Nebenläufigkeitsmodell, das auf CSP basiert, daher ähnelt es dem Nebenläufigkeitsmodell, das mehrere Interpreter unterstützen. Allerdings bietet Go auch syntaktische Unterstützung sowie mehrere integrierte Nebenläufigkeitsprimitive, um Nebenläufigkeit zu einer erstklassigen Funktion zu machen. Denkbar wäre, dass eine ähnliche syntaktische (und integrierte) Unterstützung in Python mithilfe von Interpretern hinzugefügt werden könnte. Dies liegt jedoch *weit* außerhalb des Rahmens dieser PEP!
Multiprocessing
Das Modul multiprocessing könnte Interpreter auf die gleiche Weise unterstützen, wie es Threads und Prozesse unterstützt. Tatsächlich hat der Maintainer des Moduls, Davin Potts, angegeben, dass dies eine angemessene Funktionsanforderung ist. Es liegt jedoch außerhalb des engen Umfangs dieser PEP.
C-Erweiterung Opt-in/Opt-out
Durch die Verwendung des PyModuleDef_Slot, der von PEP 489 eingeführt wurde, könnten wir leicht einen Mechanismus hinzufügen, mit dem C-Erweiterungsmodule die Unterstützung für mehrere Interpreter ablehnen können. Dann müsste die Importmaschinerie beim Betrieb in einem Subinterpreter das Modul auf Unterstützung prüfen. Es würde einen ImportError auslösen, wenn es nicht unterstützt wird.
Alternativ könnten wir die Unterstützung für die Einbeziehung mehrerer Interpreter unterstützen. Dies würde jedoch wahrscheinlich mehr Module (unnötigerweise) ausschließen als der Opt-out-Ansatz. Beachten Sie auch, dass PEP 489 definiert hat, dass die Verwendung der PEP-Mechanismen einer Erweiterung die Unterstützung für mehrere Interpreter impliziert.
Der Umfang der Hinzufügung des ModuleDef-Slots und der Anpassung der Importmaschinerie ist nicht trivial, könnte sich aber lohnen. Es hängt alles davon ab, wie viele Erweiterungsmodule unter Subinterpretern brechen. Da wir durch mod_wsgi nur relativ wenige Fälle kennen, können wir dies für später aufschieben.
Kanäle vergiften
CSP hat das Konzept der Vergiftung eines Kanals. Sobald ein Kanal vergiftet ist, würde jeder send()- oder recv()-Aufruf darauf eine spezielle Ausnahme auslösen, die effektiv die Ausführung im Interpreter beendet, der versucht hat, den vergifteten Kanal zu verwenden.
Dies könnte durch die Hinzufügung einer Methode poison() zu beiden Enden des Kanals erreicht werden. Die Methode close() kann auf diese Weise (größtenteils) verwendet werden, aber diese Semantik ist relativ spezialisiert und kann warten.
__main__ zurücksetzen
Wie vorgeschlagen, wird jeder Aufruf von Interpreter.exec() im Namensraum des bestehenden Moduls __main__ des Interpreters ausgeführt. Das bedeutet, dass die Daten dort zwischen interp.exec()-Aufrufen persistent bleiben. Manchmal ist dies nicht erwünscht und man möchte in einem frischen __main__ ausführen. Außerdem möchte man dort nicht unbedingt Objekte leaken, die man nicht mehr verwendet.
Beachten Sie, dass das Folgende nicht richtig funktioniert, da es zu viel löschen wird (z.B. __name__ und die anderen "__dunder__"-Attribute).
interp.exec('globals().clear()')
Mögliche Lösungen beinhalten:
- ein Argument
create(), um__main__nach jedeminterp.exec()-Aufruf zurückzusetzen. - ein Flag
Interpreter.reset_main, um nachträglich das Aktivieren oder Deaktivieren zu unterstützen. - eine Methode
Interpreter.reset_main(), um bei Bedarf zu aktivieren. importlib.util.reset_globals()[reset_globals].
Beachten Sie auch, dass das Zurücksetzen von __main__ nichts am Zustand anderer Module ändert. Jede Lösung müsste also klarstellen, was zurückgesetzt wird. Denkbar wäre, dass wir einen Mechanismus erfinden, mit dem jedes (oder jedes) Modul zurückgesetzt werden könnte, im Gegensatz zu reload(), das das Modul nicht vor dem Laden leert.
Unabhängig davon hat das Zurücksetzen von __main__, da es der Ausführungsnamensraum des Interpreters ist, eine viel direktere Korrelation zu den Interpretern und ihrem dynamischen Zustand als das Zurücksetzen anderer Module. Ein allgemeinerer Modulrücksetzmechanismus mag daher unnötig sein.
Dies ist anfangs keine kritische Funktion. Sie kann später, wenn gewünscht, warten.
Den Zustand eines Interpreters zurücksetzen
Es wäre praktisch, einen bestehenden Subinterpreter wiederzuverwenden, anstatt einen neuen zu starten. Da ein Interpreter deutlich mehr Zustand hat als nur das Modul __main__, ist es nicht so einfach, einen Interpreter wieder in einen makellosen/frischen Zustand zu versetzen. Tatsächlich *könnten* Teile des Zustands nicht aus Python-Code zurückgesetzt werden.
Eine mögliche Lösung ist die Hinzufügung einer Methode Interpreter.reset(). Diese würde den Interpreter in den Zustand zurückversetzen, in dem er sich befand, als er neu erstellt wurde. Wenn sie auf einen laufenden Interpreter angewendet wird, schlägt sie fehl (daher kann der Hauptinterpreter niemals zurückgesetzt werden). Dies wäre wahrscheinlich effizienter als die Erstellung eines neuen Interpreters, obwohl dies von den später vorgenommenen Optimierungen der Interpretererstellung abhängt.
Während dies potenziell Funktionalität bietet, die sonst nicht aus Python-Code verfügbar ist, ist es keine grundlegende Funktionalität. Im Sinne des Minimalismus kann dies also warten. Ungeachtet dessen bezweifle ich, dass seine Hinzufügung nach PEP kontrovers wäre.
Den Zustand eines vorhandenen Interpreters kopieren
In verwandter Weise könnte es nützlich sein, die Erstellung eines neuen Interpreters basierend auf einem bestehenden zu unterstützen, z.B. Interpreter.copy(). Dies knüpft an die Idee an, dass ein Schnappschuss des Speichers eines Interpreters erstellt werden könnte, was das Starten von CPython oder die Erstellung neuer Interpreter im Allgemeinen beschleunigen würde. Derselbe Mechanismus könnte für einen hypothetischen Interpreter.reset() verwendet werden, wie zuvor beschrieben.
Integration mit async
Laut Antoine Pitrou [async].
Has any thought been given to how FIFOs could integrate with async
code driven by an event loop (e.g. asyncio)? I think the model of
executing several asyncio (or Tornado) applications each in their
own subinterpreter may prove quite interesting to reconcile multi-
core concurrency with ease of programming. That would require the
FIFOs to be able to synchronize on something an event loop can wait
on (probably a file descriptor?).
Die grundlegende Funktionalität der Unterstützung mehrerer Interpreter hängt nicht von async ab und kann später hinzugefügt werden.
Eine mögliche Lösung ist die Bereitstellung von Async-Implementierungen der blockierenden Kanalmethoden (recv() und send()).
Alternativ könnten "readiness callbacks" verwendet werden, um die Verwendung in asynchronen Szenarien zu vereinfachen. Dies würde bedeuten, einen optionalen Parameter callback (kw-only) zu den Methoden recv_nowait() und send_nowait() des Kanals hinzuzufügen. Der Callback würde aufgerufen, sobald das Objekt gesendet oder empfangen wurde (jeweils).
(Beachten Sie, dass das Puffern von Kanälen die Bedeutung von Readiness Callbacks verringert.)
Unterstützung für Iteration
Die Unterstützung für die Iteration über RecvChannel (über __iter__() oder _next__()) könnte nützlich sein. Eine triviale Implementierung würde die Methode recv() verwenden, ähnlich wie Dateien iterieren. Da dies keine grundlegende Fähigkeit ist und ein einfaches Analogon hat, kann die Unterstützung für die Iteration später warten.
Kanal-Kontextmanager
Die Unterstützung für Kontextmanager auf RecvChannel und SendChannel könnte hilfreich sein. Die Implementierung wäre einfach und würde einen Aufruf von close() (oder vielleicht release()) umschließen, ähnlich wie bei Dateien. Wie bei der Iteration kann dies warten.
Pipes und Queues
Mit dem vorgeschlagenen Mechanismus zur Objektübergabe von "os.pipe()" sind andere ähnliche grundlegende Typen nicht unbedingt erforderlich, um die minimale nützliche Funktionalität mehrerer Interpreter zu erreichen. Solche Typen umfassen Pipes (wie unbuffered Kanäle, aber Eins-zu-Eins) und Queues (wie Kanäle, aber generischer). Siehe unten in Abgelehnte Ideen für weitere Informationen.
Auch wenn diese Typen nicht Teil dieses Vorschlags sind, können sie im Kontext der Nebenläufigkeit dennoch nützlich sein. Ihre spätere Hinzufügung ist völlig vernünftig. Sie könnten trivial als Wrapper um Kanäle implementiert werden. Alternativ könnten sie aus Effizienzgründen auf derselben niedrigen Ebene wie Kanäle implementiert werden.
Ein Lock von send() zurückgeben
Beim Senden eines Objekts über einen Kanal wissen Sie nicht, wann das Objekt am anderen Ende empfangen wird. Eine Möglichkeit, dies zu umgehen, besteht darin, einen gesperrten threading.Lock von SendChannel.send() zurückzugeben, der entsperrt wird, sobald das Objekt empfangen wurde.
Alternativ bieten die vorgeschlagenen SendChannel.send() (blockierend) und SendChannel.send_nowait() eine explizite Unterscheidung, die Benutzer wahrscheinlich weniger verwirrt.
Beachten Sie, dass die Rückgabe eines Locks für gepufferte Kanäle (d.h. Queues) relevant wäre. Für unbuffered Kanäle ist dies kein Problem.
Priorisierung in Kanälen unterstützen
Ein einfaches Beispiel ist queue.PriorityQueue in der Standardbibliothek.
Unterstützung für das Erben von Einstellungen (und mehr?)
Es könnte für Benutzer nützlich sein, beim Erstellen eines neuen Interpreters angeben zu können, dass sie einige Dinge vom neuen Interpreter "erben" möchten. Der Mechanismus könnte eine strikte Kopie oder eine Copy-on-Write sein. Das motivierende Beispiel ist das Warnungsmodul (z.B. Kopieren der Filter).
Die Funktion ist weder kritisch noch weit verbreitet, daher kann sie warten, bis Interesse besteht. Insbesondere werden beide vorgeschlagenen Lösungen erhebliche Arbeit erfordern, insbesondere bei komplexen Objekten und vor allem bei veränderlichen Containern veränderlicher komplexer Objekte.
RunFailedError.__cause__ verzögert machen
Eine nicht abgefangene Ausnahme in einem Subinterpreter (von interp.exec()) wird in den aufrufenden Interpreter kopiert und als __cause__ auf einem RunFailedError gesetzt, der dann ausgelöst wird. Dieses Kopieren beinhaltet eine Art Deserialisierung im aufrufenden Interpreter, was teuer sein kann (z.B. aufgrund von Imports) und nicht immer notwendig ist.
Daher könnte es nützlich sein, einen ExceptionProxy-Typ zu verwenden, um die serialisierte Ausnahme zu umschließen und sie erst zu deserialisieren, wenn sie benötigt wird. Dies könnte über ExceptionProxy__getattribute__() oder vielleicht über RunFailedError.resolve() geschehen (was die deserialisierte Ausnahme auslösen und RunFailedError.__cause__ auf die Ausnahme setzen würde).
Es könnte auch sinnvoll sein, dass RunFailedError.__cause__ ein Deskriptor ist, der die verzögerte Deserialisierung (und das Setzen von __cause__) auf der Instanz von RunFailedError durchführt.
Einen Wert von interp.exec() zurückgeben
Derzeit gibt interp.exec() immer None zurück. Eine Idee ist, den Rückgabewert dessen zurückzugeben, was der Subinterpreter ausgeführt hat. Vorerst macht das jedoch keinen Sinn. Das Einzige, was ausgeführt werden kann, ist ein Code-String (d.h. ein Skript). Dies entspricht PyRun_StringFlags(), exec() oder einem Modulkörper. Keine dieser Funktionen "gibt" etwas zurück. Wir können dies noch einmal überprüfen, sobald interp.exec() Funktionen usw. unterstützt.
SystemExit und KeyboardInterrupt unterschiedlich weitergeben
Die Ausnahmetypen, die von BaseException (außer Exception) erben, werden normalerweise speziell behandelt. Diese Typen sind: KeyboardInterrupt, SystemExit und GeneratorExit. Es könnte sinnvoll sein, sie speziell zu behandeln, wenn es um die Weitergabe von interp.exec() geht. Hier sind einige Optionen:
* propagate like normal via RunFailedError
* do not propagate (handle them somehow in the subinterpreter)
* propagate them directly (avoid RunFailedError)
* propagate them directly (set RunFailedError as __cause__)
Wir werden uns nicht darum kümmern, sie anders zu behandeln. Threads ignorieren bereits SystemExit, also werden wir vorerst diesem Muster folgen.
Ein explizites release() und close() zu Channel-End-Klassen hinzufügen
Es kann praktisch sein, eine explizite Möglichkeit zu haben, einen Kanal für weitere globale Nutzung zu schließen. Ebenso könnte es nützlich sein, eine explizite Möglichkeit zu haben, ein Ende des Kanals relativ zum aktuellen Interpreter freizugeben. Unter anderem ist ein solcher Mechanismus nützlich, um den Gesamtstatus zwischen Interpretern zu kommunizieren, ohne den zusätzlichen Boilerplate-Code, der das direkte Übergeben von Objekten über einen Kanal erfordern würde.
Die Herausforderung besteht darin, die automatische Freigabe/Schließung korrekt zu gestalten, ohne sie schwer verständlich zu machen. Dies gilt insbesondere bei einem nicht leeren Kanal. Wir sollten vorerst ohne Freigabe/Schließung auskommen.
SendChannel.send_buffer() hinzufügen
Diese Methode würde das Senden eines Objekts über einen Kanal ohne Kopie ermöglichen, wenn es das PEP 3118-Pufferprotokoll unterstützt (z.B. memoryview).
Die Unterstützung dafür ist für Kanäle nicht grundlegend und kann später ohne größere Störung hinzugefügt werden.
Automatisch in einem Thread ausführen
Der PEP schlägt eine klare Trennung zwischen Subinterpretern und Threads vor: Wenn Sie in einem Thread ausführen möchten, müssen Sie den Thread selbst erstellen und interp.exec() darin aufrufen. Es könnte jedoch praktisch sein, wenn interp.exec() dies für Sie tun könnte, was weniger Boilerplate bedeuten würde.
Darüber hinaus gehen wir davon aus, dass Benutzer viel öfter in einem Thread ausführen möchten als nicht. Daher wäre es sinnvoll, dies zum Standardverhalten zu machen. Wir würden einen kw-only Parameter "threaded" (Standard True) zu interp.exec() hinzufügen, um den Vorgang im aktuellen Thread zu ermöglichen.
Abgelehnte Ideen
Explizite Kanalassoziation
Interpreter werden bei recv()- und send()-Aufrufen implizit mit Kanalenden assoziiert. Sie werden bei release()-Aufrufen de-assoziiert. Die Alternative wären explizite Methoden. Es wären entweder Methoden add_channel() und remove_channel() für Interpreter-Objekte oder etwas Ähnliches für Kanalobjekte.
In der Praxis sollte dieses Verwaltungsniveau für Benutzer nicht notwendig sein. Das Hinzufügen von expliziterer Unterstützung würde die API nur unnötig aufblähen.
Eine API basierend auf Pipes hinzufügen
Eine Pipe wäre ein Simplex-FIFO zwischen genau zwei Interpretern. Für die meisten Anwendungsfälle wäre dies ausreichend. Es könnte potenziell auch die Implementierung vereinfachen. Es ist jedoch kein großer Schritt, einen Many-to-Many-Simplex-FIFO über Kanäle zu unterstützen. Auch bei Pipes wird die API etwas komplizierter, da die Pipes benannt werden müssen.
Eine API basierend auf Queues hinzufügen
Queues und gepufferte Kanäle sind fast dasselbe. Der Hauptunterschied besteht darin, dass Kanäle eine stärkere Beziehung zum Kontext (d.h. dem assoziierten Interpreter) haben.
Der Name "Channel" wurde anstelle von "Queue" verwendet, um Verwechslungen mit der Standardbibliothek queue.Queue zu vermeiden.
„enumerate“
Die Funktion list_all() liefert die Liste aller Interpreter. Im Threading-Modul, das die vorgeschlagene API teilweise inspiriert hat, heißt die Funktion enumerate(). Der Name ist hier anders, um Python-Benutzer, die nicht mit der Threading-API vertraut sind, nicht zu verwirren. Für sie ist "enumerate" eher unklar, während "list_all" klar ist.
Alternative Lösungen zur Verhinderung von Ausnahmenlecks zwischen Interpretern
In Funktionsaufrufen propagieren nicht abgefangene Ausnahmen in den aufrufenden Frame. Derselbe Ansatz könnte mit interp.exec() verfolgt werden. Dies würde jedoch bedeuten, dass Ausnameobjekte die Inter-Interpreter-Grenze überschreiten. Ebenso könnten die Frames im Traceback überschreiten.
Das mag derzeit kein Problem sein, wäre aber ein Problem, sobald Interpreter eine bessere Isolation in Bezug auf die Speicherverwaltung erhalten (was notwendig ist, um die gemeinsame Nutzung des GIL zwischen Interpretern zu beenden). Wir haben die Semantik der Ausnahmenausbreitung gelöst, indem wir stattdessen eine RunFailedError auslösen, bei der __cause__ einen sicheren Proxy für die ursprüngliche Ausnahme und den Traceback umschließt.
Abgelehnte mögliche Lösungen:
- die Ausnahme und den Traceback im ursprünglichen Interpreter reproduzieren und auslösen.
- eine Unterklasse von RunFailedError auslösen, die die ursprüngliche Ausnahme und den Traceback proxy-isiert.
- RuntimeError statt RunFailedError auslösen.
- an der Grenze konvertieren (a la
subprocess.CalledProcessError) (erfordert eine cross-Interpreter-Darstellung). - Anpassung über
Interpreter.excepthookunterstützen (erfordert eine cross-Interpreter-Darstellung). - an der Grenze in einen Proxy einschließen (einschließlich Unterstützung für etwas wie
err.raise(), um den Traceback zu propagieren). - die Ausnahme (oder ihren Proxy) von
interp.exec()zurückgeben, anstatt sie auszulösen. - ein Ergebnisobjekt zurückgeben (wie
subprocesses tut) [result-object] (unnötige Komplexität?). - die Ausnahme verwerfen und den Benutzern erwarten, dass sie nicht abgefangene Ausnahmen explizit in dem Skript behandeln, das sie an
interp.exec()übergeben (sie können Fehlerinformationen über Kanäle nach außen übergeben); mit Threads muss man etwas Ähnliches tun.
Jeden neuen Interpreter immer mit seinem eigenen Thread verbinden
Wie in der C-API implementiert, ist ein Interpreter nicht inhärent an einen Thread gebunden. Darüber hinaus läuft er in jedem vorhandenen Thread, egal ob er von Python erstellt wurde oder nicht. Sie müssen nur zuerst einen seiner Thread-Zustände (PyThreadState) in dem Thread aktivieren. Das bedeutet, dass derselbe Thread mehr als einen Interpreter ausführen kann (wenn auch offensichtlich nicht gleichzeitig).
Das vorgeschlagene Modul behält dieses Verhalten bei. Interpreter sind nicht an Threads gebunden. Nur Aufrufe von Interpreter.exec() sind es. Ein Hauptziel dieser PEP ist es jedoch, ein menschzentrierteres Nebenläufigkeitsmodell bereitzustellen. Mit diesem im Hinterkopf könnte das Modul aus konzeptioneller Sicht einfacher zu verstehen sein, wenn jeder Interpreter mit einem eigenen Thread assoziiert wäre.
Das würde bedeuten, dass interpreters.create() einen neuen Thread erstellt und Interpreter.exec() nur in diesem Thread ausgeführt wird (und nichts anderes). Der Vorteil ist, dass Benutzer Interpreter.exec()-Aufrufe nicht in einen neuen threading.Thread einwickeln müssten. Sie wären auch nicht in der Lage, versehentlich den aktuellen Interpreter (im aktuellen Thread) anzuhalten, während ihr Interpreter ausgeführt wird.
Die Idee wird verworfen, da der Vorteil gering und die Kosten hoch sind. Der Unterschied zur Funktionalität in der C-API wäre potenziell verwirrend. Die implizite Erstellung von Threads ist magisch. Die frühe Erstellung von Threads ist potenziell verschwenderisch. Die Unfähigkeit, beliebige Interpreter in einem vorhandenen Thread auszuführen, würde einige gültige Anwendungsfälle verhindern und Benutzer frustrieren. Die Bindung von Interpretern an Threads würde zusätzliche Laufzeitmodifikationen erfordern. Sie würde auch die Implementierung des Moduls übermäßig komplizieren. Schließlich könnte sie das Modul nicht einmal einfacher zu verstehen machen.
Interpreter nur bei Verwendung verbinden
Interpreter nur dann mit Kanalenden assoziieren, wenn recv(), send() usw. aufgerufen werden.
Dies ist potenziell verwirrend und kann auch zu unerwarteten Wettläufen führen, bei denen ein Kanal automatisch geschlossen wird, bevor er im ursprünglichen (erstellenden) Interpreter verwendet werden kann.
Mehrere gleichzeitige Aufrufe von Interpreter.exec() zulassen
Dies wäre insbesondere dann sinnvoll, wenn Interpreter.exec() neue Threads für Sie verwalten würde (was wir abgelehnt haben). Im Wesentlichen würde jeder Aufruf unabhängig ausgeführt, was aus einer engen technischen Perspektive meist in Ordnung wäre, da jeder Interpreter mehrere Threads haben kann.
Das Problem ist, dass der Interpreter nur ein einziges __main__-Modul hat und gleichzeitige Interpreter.exec()-Aufrufe __main__ teilen müssten oder wir müssten einen neuen Mechanismus erfinden. Keines davon wäre einfach genug, um sich lohnen zu würde.
Eine „reraise“-Methode zu RunFailedError hinzufügen
Während __cause__ bei RunFailedError gesetzt ist und zu einer nützlicheren Traceback beiträgt, ist es bei der Behandlung des ursprünglichen Fehlers weniger hilfreich. Um dies zu erleichtern, könnten wir RunFailedError.reraise() hinzufügen. Diese Methode würde das folgende Muster ermöglichen
try:
try:
interp.exec(script)
except RunFailedError as exc:
exc.reraise()
except MyException:
...
Dies würde noch einfacher gemacht, wenn es ein __reraise__-Protokoll gäbe.
All das gesagt, ist dies völlig unnötig. Die Verwendung von __cause__ ist gut genug
try:
try:
interp.exec(script)
except RunFailedError as exc:
raise exc.__cause__
except MyException:
...
Beachten Sie, dass dies in extremen Fällen etwas zusätzlichen Boilerplate-Code erfordern kann
try:
try:
interp.exec(script)
except RunFailedError as exc:
if exc.__cause__ is not None:
raise exc.__cause__
raise # re-raise
except MyException:
...
Implementierung
Die Implementierung des PEP hat 4 Teile
- das übergeordnete Modul, das in diesem PEP beschrieben wird (hauptsächlich ein leichter Wrapper um eine C-Erweiterung auf niedriger Ebene
- das C-Erweiterungsmodul auf niedriger Ebene
- Ergänzungen zur internen C-API, die vom Modul auf niedriger Ebene benötigt werden
- sekundäre Korrekturen/Änderungen im CPython-Laufzeitsystem, die das Modul auf niedriger Ebene erleichtern (neben anderen Vorteilen)
Diese befinden sich in unterschiedlichen Stadien der Fertigstellung, wobei je weiter man nach unten geht, desto mehr ist erledigt
- Das übergeordnete Modul wurde bestenfalls grob implementiert. Die vollständige Implementierung wird jedoch fast trivial sein.
- Das Modul auf niedriger Ebene ist größtenteils fertig. Der Großteil der Implementierung wurde im Dezember 2018 als Modul "_xxsubinterpreters" in den Master-Zweig integriert (um das Testen von mehreren Interpreterfunktionalitäten zu ermöglichen). Nur die Implementierung der Ausnahme-Weitergabe muss noch fertiggestellt werden, was keine umfangreiche Arbeit erfordert.
- Alle notwendigen C-API-Arbeiten sind abgeschlossen
- Alle erwarteten Arbeiten in der Laufzeitumgebung sind abgeschlossen
Der Implementierungsaufwand für PEP 554 wird als Teil eines größeren Projekts zur Verbesserung der Multi-Core-Unterstützung in CPython verfolgt. [multi-core-project]
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0554.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT