PEP 419 – Schutz von Bereinigungsanweisungen vor Unterbrechungen
- Autor:
- Paul Colomiets <paul at colomiets.name>
- Status:
- Verschoben
- Typ:
- Standards Track
- Erstellt:
- 06-Apr-2012
- Python-Version:
- 3.3
Zusammenfassung
Dieses PEP schlägt einen Weg vor, Python-Code vor Unterbrechungen innerhalb einer finally-Klausel oder während der Bereinigung von Kontextmanagern zu schützen.
PEP Verschiebung
Die weitere Erforschung der in diesem PEP behandelten Konzepte wurde mangels eines aktuellen Vertreters, der an der Förderung der Ziele des PEP interessiert ist und Feedback sammelt und einarbeitet, sowie über ausreichend verfügbare Zeit zur effektiven Umsetzung, zurückgestellt.
Begründung
Python bietet zwei gute Möglichkeiten zur Bereinigung. Eine ist die finally-Anweisung und die andere ist ein Kontextmanager (üblicherweise mittels einer with-Anweisung verwendet). Jedoch sind beide nicht vor Unterbrechungen durch KeyboardInterrupt oder GeneratorExit, verursacht durch generator.throw(), geschützt. Zum Beispiel:
lock.acquire()
try:
print('starting')
do_something()
finally:
print('finished')
lock.release()
Wenn KeyboardInterrupt direkt nach dem zweiten print()-Aufruf auftritt, wird das Schloss nicht freigegeben. Ähnlich ist der folgende Code, der die with-Anweisung verwendet, betroffen.
from threading import Lock
class MyLock:
def __init__(self):
self._lock_impl = Lock()
def __enter__(self):
self._lock_impl.acquire()
print("LOCKED")
def __exit__(self):
print("UNLOCKING")
self._lock_impl.release()
lock = MyLock()
with lock:
do_something
Wenn KeyboardInterrupt in der Nähe eines der print()-Aufrufe auftritt, wird das Schloss niemals freigegeben.
Coroutine-Anwendungsfall
Ein ähnlicher Fall tritt bei Coroutinen auf. Normalerweise möchten Coroutinenbibliotheken eine Coroutine mit einem Timeout unterbrechen. Die Methode generator.throw() funktioniert für diesen Anwendungsfall, aber es gibt keine Möglichkeit zu wissen, ob die Coroutine derzeit aus einer finally-Klausel heraus ausgesetzt ist.
Ein Beispiel, das Yield-basierte Coroutinen verwendet, folgt. Der Code sieht bei Verwendung einer der populären Coroutinenbibliotheken Monocle [1], Bluelet [2] oder Twisted [3] ähnlich aus.
def run_locked():
yield connection.sendall('LOCK')
try:
yield do_something()
yield do_something_else()
finally:
yield connection.sendall('UNLOCK')
with timeout(5):
yield run_locked()
Im obigen Beispiel bedeutet yield something das Pausieren der Ausführung der aktuellen Coroutine und die Ausführung der Coroutine something, bis diese abgeschlossen ist. Daher muss die Coroutinenbibliothek selbst einen Stack von Generatoren verwalten. Der Aufruf connection.sendall() wartet, bis der Socket schreibbar ist, und tut etwas Ähnliches wie socket.sendall().
Die with-Anweisung stellt sicher, dass der gesamte Code innerhalb eines 5-Sekunden-Timeouts ausgeführt wird. Dies geschieht durch Registrierung eines Callbacks in der Hauptschleife, die generator.throw() auf dem obersten Frame im Coroutinen-Stack aufruft, wenn ein Timeout auftritt.
Die Erweiterung greenlets funktioniert auf ähnliche Weise, außer dass sie kein yield benötigt, um in einen neuen Stack-Frame einzutreten. Ansonsten sind die Überlegungen ähnlich.
Spezifikation
Frame-Flag ‘f_in_cleanup’
Ein neues Flag im Frame-Objekt wird vorgeschlagen. Es wird auf True gesetzt, wenn dieser Frame gerade eine finally-Klausel ausführt. Intern muss das Flag als Zähler verschachtelter finally-Anweisungen implementiert werden, die gerade ausgeführt werden.
Der interne Zähler muss auch während der Ausführung der Bytecodes SETUP_WITH und WITH_CLEANUP inkrementiert und beim Beenden dieser Bytecodes dekrementiert werden. Dies ermöglicht auch den Schutz von __enter__() und __exit__() Methoden.
Funktion ‘sys.setcleanuphook’
Eine neue Funktion für das sys-Modul wird vorgeschlagen. Diese Funktion setzt einen Callback, der jedes Mal ausgeführt wird, wenn f_in_cleanup falsch wird. Callbacks erhalten ein Frame-Objekt als einziges Argument, damit sie herausfinden können, woher sie aufgerufen werden.
Die Einstellung ist Thread-lokal und muss in der PyThreadState-Struktur gespeichert werden.
Erweiterungen des Inspect-Moduls
Zwei neue Funktionen werden für das inspect-Modul vorgeschlagen: isframeincleanup() und getcleanupframe().
isframeincleanup() gibt, gegeben ein Frame- oder Generator-Objekt als einziges Argument, den Wert des f_in_cleanup-Attributs des Frames selbst oder des gi_frame-Attributs eines Generators zurück.
getcleanupframe() gibt, gegeben ein Frame-Objekt als einziges Argument, den innersten Frame zurück, der einen wahren Wert für f_in_cleanup hat, oder None, wenn kein Frame im Stack einen Wert ungleich Null für dieses Attribut hat. Es beginnt die Inspektion vom angegebenen Frame aus und wandert mit f_back-Zeigern zu äußeren Frames, genau wie getouterframes().
Beispiel
Eine beispielhafte Implementierung eines SIGINT-Handlers, der sicher unterbricht, könnte so aussehen:
import inspect, sys, functools
def sigint_handler(sig, frame):
if inspect.getcleanupframe(frame) is None:
raise KeyboardInterrupt()
sys.setcleanuphook(functools.partial(sigint_handler, 0))
Ein Coroutinenbeispiel liegt außerhalb des Rahmens dieses Dokuments, da seine Implementierung sehr stark von einer Trampolin- (oder Hauptschleifen-) Struktur abhängt, die von der Coroutinenbibliothek verwendet wird.
Ungelöste Probleme
Unterbrechung innerhalb eines With-Anweisungs-Ausdrucks
Gegeben die Aussage:
with open(filename):
do_something()
Python kann nach dem Aufruf von open() unterbrochen werden, aber bevor der SETUP_WITH-Bytecode ausgeführt wird. Es gibt zwei mögliche Entscheidungen:
- Schützen von
with-Ausdrücken. Dies würde einen weiteren Bytecode erfordern, da es derzeit keine Möglichkeit gibt, den Beginn deswith-Ausdrucks zu erkennen. - Dem Benutzer erlauben, eine Wrapper-Funktion zu schreiben, wenn er dies für den Anwendungsfall als wichtig erachtet. Ein sicherer Wrapper könnte so aussehen:
class FileWrapper(object): def __init__(self, filename, mode): self.filename = filename self.mode = mode def __enter__(self): self.file = open(self.filename, self.mode) def __exit__(self): self.file.close()
Alternativ kann es mit dem
contextmanager()-Decorator geschrieben werden:@contextmanager def open_wrapper(filename, mode): file = open(filename, mode) try: yield file finally: file.close()
Dieser Code ist sicher, da der erste Teil des Generators (vor yield) innerhalb des
SETUP_WITH-Bytecodes des Aufrufers ausgeführt wird.
Ausnahmeausbreitung
Manchmal kann eine finally-Klausel oder eine __enter__()/__exit__()-Methode eine Ausnahme auslösen. Normalerweise ist dies kein Problem, da wichtigere Ausnahmen wie KeyboardInterrupt oder SystemExit stattdessen ausgelöst werden sollten. Aber es könnte nützlich sein, die ursprüngliche Ausnahme im __context__-Attribut zu behalten. Daher kann die Signatur des Bereinigungs-Hooks um ein Ausnahmeargument erweitert werden:
def sigint_handler(sig, frame)
if inspect.getcleanupframe(frame) is None:
raise KeyboardInterrupt()
sys.setcleanuphook(retry_sigint)
def retry_sigint(frame, exception=None):
if inspect.getcleanupframe(frame) is None:
raise KeyboardInterrupt() from exception
Hinweis
Es ist nicht notwendig, drei Argumente wie bei der __exit__-Methode zu haben, da es in Python 3 ein __traceback__-Attribut in der Ausnahme gibt.
Dies setzt jedoch die __cause__ für die Ausnahme, was nicht genau das ist, was beabsichtigt ist. Daher kann eine versteckte Interpreterlogik verwendet werden, um jedem in einem Bereinigungs-Hook ausgelösten Ausnahme ein __context__-Attribut zuzuweisen.
Unterbrechung zwischen dem Erwerb einer Ressource und dem Try-Block
Das Beispiel aus dem ersten Abschnitt ist nicht ganz sicher. Betrachten wir es genauer:
lock.acquire()
try:
do_something()
finally:
lock.release()
Das Problem kann auftreten, wenn der Code gerade nach der Ausführung von lock.acquire(), aber vor dem Eintritt in den try-Block unterbrochen wird.
Es gibt keine Möglichkeit, den Code unverändert zu reparieren. Die tatsächliche Reparatur hängt stark vom Anwendungsfall ab. Normalerweise kann Code mit einer with-Anweisung behoben werden:
with lock:
do_something()
Bei Coroutinen kann man jedoch normalerweise die with-Anweisung nicht verwenden, da man für die Acquire- und Release-Operationen yielden muss. Der Code könnte also umgeschrieben werden:
try:
yield lock.acquire()
do_something()
finally:
yield lock.release()
Der eigentliche Sperrcode benötigt möglicherweise zusätzlichen Code, um diesen Anwendungsfall zu unterstützen, aber die Implementierung ist in der Regel trivial, so: prüfen, ob das Schloss erworben wurde, und es freigeben, wenn es erworben wurde.
Behandlung von EINTR innerhalb eines Finally
Selbst wenn ein Signal-Handler darauf vorbereitet ist, das f_in_cleanup-Flag zu überprüfen, kann InterruptedError im Bereinigungs-Handler ausgelöst werden, da der entsprechende Systemaufruf einen EINTR-Fehler zurückgegeben hat. Die primären Anwendungsfälle sind darauf vorbereitet:
- POSIX-Mutexes geben niemals
EINTRzurück. - Netzwerkbibliotheken sind immer darauf vorbereitet,
EINTRzu behandeln. - Coroutinenbibliotheken werden normalerweise mit der
throw()-Methode unterbrochen, nicht mit einem Signal.
Die plattformspezifische Funktion siginterrupt() könnte verwendet werden, um die Notwendigkeit der Behandlung von EINTR zu beseitigen. Sie kann jedoch kaum vorhersehbare Konsequenzen haben, zum Beispiel wird ein SIGINT-Handler niemals aufgerufen, wenn der Hauptthread in einer I/O-Routine blockiert ist.
Ein besserer Ansatz wäre es, den Code, der üblicherweise in Bereinigungs-Handlern verwendet wird, darauf vorzubereiten, InterruptedError explizit zu behandeln. Ein Beispiel für einen solchen Code wäre eine dateibasierte Sperrimpementierung.
signal.pthread_sigmask kann verwendet werden, um Signale innerhalb von Bereinigungs-Handlern zu blockieren, die mit EINTR unterbrochen werden können.
Setzen des Unterbrechungskontexts innerhalb des Finally selbst
Einige Coroutinenbibliotheken müssen möglicherweise ein Timeout für die finally-Klausel selbst festlegen. Zum Beispiel:
try:
do_something()
finally:
with timeout(0.5):
try:
yield do_slow_cleanup()
finally:
yield do_fast_cleanup()
Mit den aktuellen Semantiken wird das Timeout entweder den gesamten with-Block schützen oder überhaupt nichts, abhängig von der Implementierung jeder Bibliothek. Was der Autor beabsichtigte, ist do_slow_cleanup als normalen Code zu behandeln und do_fast_cleanup als Bereinigung (eine nicht unterbrechbare)..
Ein ähnlicher Fall kann beim Verwenden von Greenlets oder Tasklets auftreten.
Dieser Fall kann durch das Exponieren von f_in_cleanup als Zähler und durch das Aufrufen eines Bereinigungs-Hooks bei jeder Dekrementierung behoben werden. Eine Coroutinenbibliothek kann dann den Wert beim Start des Timeouts merken und ihn bei jeder Hook-Ausführung vergleichen.
Aber in der Praxis wird das Beispiel als zu obskur betrachtet, um berücksichtigt zu werden.
Modifizierung von KeyboardInterrupt
Es muss entschieden werden, ob der Standard- SIGINT-Handler modifiziert werden soll, um den beschriebenen Mechanismus zu verwenden. Der ursprüngliche Vorschlag ist, das alte Verhalten beizubehalten, aus zwei Gründen:
- Die meisten Anwendungen kümmern sich nicht um die Bereinigung beim Beenden (entweder sie haben keinen externen Zustand oder sie ändern ihn auf absturzsichere Weise).
- Die Bereinigung kann zu viel Zeit in Anspruch nehmen und dem Benutzer keine Möglichkeit geben, eine Anwendung zu unterbrechen.
Letzterer Fall kann behoben werden, indem ein unsicheres Brechen erlaubt wird, wenn ein SIGINT-Handler zweimal aufgerufen wird, aber dies scheint die Komplexität nicht wert zu sein.
Unterstützung alternativer Python-Implementierungen
Wir betrachten f_in_cleanup als Implementierungsdetail. Die tatsächliche Implementierung kann ein gefälschtes Frame-ähnliches Objekt haben, das an den Signal-Handler, den Bereinigungs-Hook und von getcleanupframe() zurückgegeben wird. Die einzige Anforderung ist, dass die Funktionen des inspect-Moduls auf diesen Objekten wie erwartet funktionieren. Aus diesem Grund erlauben wir auch die Übergabe eines Generator-Objekts an die Funktion isframeincleanup(), was die Notwendigkeit der Verwendung des gi_frame-Attributs beseitigt.
Es kann notwendig sein zu spezifizieren, dass getcleanupframe() dasselbe Objekt zurückgeben muss, das beim nächsten Aufruf an den Bereinigungs-Hook übergeben wird.
Alternative Namen
Der ursprüngliche Vorschlag hatte ein f_in_finally Frame-Attribut, da die ursprüngliche Absicht war, finally-Klauseln zu schützen. Aber da es sich auf den Schutz von __enter__ und __exit__ Methoden erstreckte, scheint der Name f_in_cleanup besser zu sein. Obwohl die __enter__-Methode keine Bereinigungsroutine ist, bezieht sie sich zumindest auf die Bereinigung, die von Kontextmanagern durchgeführt wird.
setcleanuphook, isframeincleanup und getcleanupframe können zu set_cleanup_hook, is_frame_in_cleanup und get_cleanup_frame unobscured werden, obwohl sie der Namenskonvention ihrer jeweiligen Module folgen.
Alternative Vorschläge
Automatisches Weitergeben des ‘f_in_cleanup’-Flags
Dies kann getcleanupframe() überflüssig machen. Aber für Yield-basierte Coroutinen muss man es selbst weitergeben. Das schreibbar machen führt zu einem etwas unvorhersehbaren Verhalten von setcleanuphook().
Hinzufügen von Bytecodes ‘INCR_CLEANUP’, ‘DECR_CLEANUP’
Diese Bytecodes können verwendet werden, um den Ausdruck innerhalb der with-Anweisung zu schützen, sowie um Zählerinkrementierungen expliziter und leichter zu debuggen zu machen (sichtbar in einer Disassemblierung). Ein Mittelweg könnte gewählt werden, wie END_FINALLY und SETUP_WITH, die den Zähler implizit dekrementieren (END_FINALLY ist am Ende jeder with-Suite vorhanden).
Das Hinzufügen neuer Bytecodes muss jedoch sehr sorgfältig geprüft werden.
Exponieren von ‘f_in_cleanup’ als Zähler
Die ursprüngliche Absicht war, ein Minimum an benötigter Funktionalität freizugeben. Da wir jedoch das Frame-Flag f_in_cleanup als Implementierungsdetail betrachten, können wir es als Zähler freigeben.
Ebenso, wenn wir einen Zähler haben, müssen wir den Bereinigungs-Hook bei jeder Zählerdekrementierung aufrufen. Es ist unwahrscheinlich, dass dies große Leistungseinbußen hat, da verschachtelte Finally-Klauseln ein seltener Fall sind.
Hinzufügen des Code-Objekt-Flags ‘CO_CLEANUP’
Als Alternative zum Setzen des Flags innerhalb der SETUP_WITH und WITH_CLEANUP Bytecodes können wir ein Flag CO_CLEANUP einführen. Wenn der Interpreter beginnt, Code mit gesetztem CO_CLEANUP auszuführen, setzt er f_in_cleanup für den gesamten Funktionskörper. Dieses Flag wird für Code-Objekte der speziellen Methoden __enter__ und __exit__ gesetzt. Technisch könnte es auf Funktionen gesetzt werden, die __enter__ und __exit__ genannt werden.
Dies scheint eine weniger klare Lösung zu sein. Sie deckt auch den Fall ab, in dem __enter__ und __exit__ manuell aufgerufen werden. Dies kann entweder als Feature oder als unnötiger Nebeneffekt (oder, obwohl unwahrscheinlich, als Fehler) akzeptiert werden.
Es kann auch ein Problem darstellen, wenn die Funktionen __enter__ oder __exit__ in C implementiert sind, da es kein Code-Objekt gibt, um das f_in_cleanup-Flag zu überprüfen.
Bereinigungs-Callback am Frame-Objekt selbst haben
Das Frame-Objekt kann erweitert werden, um einen f_cleanup_callback-Member zu haben, der aufgerufen wird, wenn f_in_cleanup auf 0 zurückgesetzt wird. Dies würde helfen, verschiedene Callbacks für verschiedene Coroutinen zu registrieren.
Trotz seiner scheinbaren Eleganz fügt diese Lösung nichts hinzu, da die beiden primären Anwendungsfälle sind:
- Das Setzen des Callbacks in einem Signal-Handler. Der Callback ist für diesen Fall im Wesentlichen einzeln.
- Verwenden eines einzelnen Callbacks pro Schleife für den Coroutinen-Anwendungsfall. Hier gibt es in fast allen Fällen nur eine Schleife pro Thread.
Kein Bereinigungs-Hook
Der ursprüngliche Vorschlag enthielt keine Spezifikation für einen Bereinigungs-Hook, da es einige Möglichkeiten gibt, dies mit den aktuellen Werkzeugen zu erreichen:
- Verwenden von
sys.settrace()und demf_trace-Callback. Dies kann Probleme beim Debugging verursachen und hat große Leistungseinbußen (obwohl Unterbrechungen nicht sehr oft vorkommen). - Etwas länger schlafen und es erneut versuchen. Für eine Coroutinenbibliothek ist dies einfach. Für Signale kann dies mit
signal.alerterreicht werden.
Beide Methoden werden als zu unpraktisch betrachtet und ein Weg, um das Beenden von finally-Klauseln abzufangen, wird vorgeschlagen.
Referenzen
[4] Ursprüngliche Diskussion https://mail.python.org/pipermail/python-ideas/2012-April/014705.html
[5] Implementierung von PEP 419 https://github.com/python/cpython/issues/58935
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0419.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT