PEP 3153 – Unterstützung für asynchrone E/A
- Autor:
- Laurens Van Houtven <_ at lvh.cc>
- Status:
- Abgelöst
- Typ:
- Standards Track
- Erstellt:
- 29. Mai 2011
- Post-History:
- Ersetzt-Durch:
- 3156
Zusammenfassung
Dieses PEP beschreibt eine Abstraktion für asynchrone E/A für die Standardbibliothek von Python.
Das Ziel ist es, eine Abstraktion zu erreichen, die von vielen verschiedenen asynchronen E/A-Backends implementiert werden kann und eine Zielvorgabe für Bibliotheksentwickler darstellt, um codeübergreifend portablen Code zwischen diesen verschiedenen Backends zu schreiben.
Begründung
Leute, die derzeit asynchronen Code in Python schreiben möchten, haben einige Optionen
asyncoreundasynchat- etwas Maßgeschneidertes, höchstwahrscheinlich basierend auf dem
selectModul - die Verwendung einer Drittanbieterbibliothek, wie z. B. Twisted oder gevent
Leider hat jede dieser Optionen ihre Nachteile, die dieses PEP zu beheben versucht.
Obwohl das asyncore-Modul seit langem Teil der Standardbibliothek von Python ist, leidet es unter grundlegenden Mängeln, die sich aus einer unflexiblen API ergeben, die den Erwartungen eines modernen asynchronen Netzwerkmoduls nicht gerecht wird.
Darüber hinaus ist sein Ansatz zu vereinfacht, um Entwicklern alle Werkzeuge zur Verfügung zu stellen, die sie benötigen, um das Potenzial des asynchronen Netzwerkens voll auszuschöpfen.
Die derzeit beliebteste Lösung, die in der Produktion eingesetzt wird, beinhaltet die Verwendung von Drittanbieterbibliotheken. Diese bieten oft zufriedenstellende Lösungen, aber es fehlt an Kompatibilität zwischen diesen Bibliotheken, was dazu führt, dass Codebasen sehr stark an die verwendete Bibliothek gekoppelt sind.
Dieser aktuelle Mangel an Portabilität zwischen verschiedenen asynchronen E/A-Bibliotheken verursacht viel duplizierten Aufwand für Entwickler von Drittanbieterbibliotheken. Eine ausreichend leistungsfähige Abstraktion könnte bedeuten, dass asynchroner Code einmal geschrieben wird, aber überall verwendet werden kann.
Ein zukünftiges zusätzliches Ziel wäre, dass sich Implementierungen von Wire- und Netzwerkprotokollen in der Standardbibliothek zu echten Protokollimplementierungen entwickeln, anstatt zu eigenständigen Bibliotheken, die alles tun, einschließlich des blockierenden Aufrufs von recv(). Das bedeutet, dass sie leicht für synchronen und asynchronen Code wiederverwendet werden könnten.
Kommunikationsabstraktionen
Transporte
Transporte bieten eine einheitliche API zum Lesen von Bytes von und zum Schreiben von Bytes zu verschiedenen Arten von Verbindungen. Transporte in diesem PEP sind immer geordnete, zuverlässige, bidirektionale, Stream-orientierte Verbindungen mit zwei Endpunkten. Dies kann ein TCP-Socket, eine SSL-Verbindung, eine Pipe (benannt oder anderweitig), eine serielle Schnittstelle sein ... Es kann einen File Descriptor unter POSIX-Plattformen oder ein Handle unter Windows oder eine andere geeignete Datenstruktur für eine bestimmte Plattform abstrahieren. Es kapselt alle spezifischen Implementierungsdetails der Verwendung dieser plattformspezifischen Datenstruktur und stellt eine einheitliche Schnittstelle für Anwendungsentwickler bereit.
Transporte kommunizieren mit zwei Dingen: einerseits mit der anderen Seite der Verbindung und andererseits mit einem Protokoll. Sie sind eine Brücke zwischen dem spezifischen zugrunde liegenden Übertragungsmechanismus und dem Protokoll. Ihre Aufgabe besteht darin, dem Protokoll zu ermöglichen, einfach Bytes zu senden und zu empfangen, wobei alle Magie, die mit diesen Bytes geschehen muss, um sie schließlich über das Netz zu senden, übernommen wird.
Das Hauptmerkmal eines Transports ist das Senden von Bytes an ein Protokoll und das Empfangen von Bytes vom zugrunde liegenden Protokoll. Das Schreiben in den Transport erfolgt über die Methoden write und write_sequence. Letztere Methode ist eine Performance-Optimierung, die es Software ermöglicht, spezifische Fähigkeiten bestimmter Transportmechanismen zu nutzen. Insbesondere erlaubt dies Transporten die Verwendung von writev anstelle von write oder send, auch bekannt als Scatter/Gather-E/A.
Ein Transport kann angehalten und fortgesetzt werden. Dies führt dazu, dass Daten vom Protokoll gepuffert und empfangene Daten nicht mehr an das Protokoll gesendet werden.
Ein Transport kann auch geschlossen, halb geschlossen und abgebrochen werden. Ein geschlossener Transport beendet das Schreiben aller in ihm gespeicherten Daten in den zugrunde liegenden Mechanismus und stoppt dann das Lesen oder Schreiben von Daten. Das Abbrechen eines Transports stoppt ihn, schließt die Verbindung, ohne noch gepufferte Daten zu senden.
Weitere Schreibvorgänge führen zu Ausnahmen. Ein halb geschlossener Transport kann nicht mehr beschrieben werden, akzeptiert aber weiterhin eingehende Daten.
Protokolle
Protokolle sind für neue Benutzer wahrscheinlich vertrauter. Die Terminologie stimmt mit dem überein, was man von etwas namens Protokoll erwarten würde: Die Protokolle, an die die meisten Leute zuerst denken, wie HTTP, IRC, SMTP... sind alles Beispiele für etwas, das in einem Protokoll implementiert würde.
Die kürzeste nützliche Definition eines Protokolls ist eine (normalerweise zweiseitige) Brücke zwischen dem Transport und der restlichen Anwendungslogik. Ein Protokoll empfängt Bytes von einem Transport und übersetzt diese Informationen in ein bestimmtes Verhalten, das typischerweise zu einigen Methodenaufrufen auf einem Objekt führt. Ebenso ruft die Anwendungslogik einige Methoden auf dem Protokoll auf, das das Protokoll in Bytes übersetzt und dem Transport mitteilt.
Eines der einfachsten Protokolle ist ein zeilenbasiertes Protokoll, bei dem Daten durch \r\n getrennt sind. Das Protokoll empfängt Bytes vom Transport und puffert sie, bis mindestens eine vollständige Zeile vorhanden ist. Sobald dies geschehen ist, leitet es diese Zeile an ein Objekt weiter. Idealerweise würde dies mit einem Callable oder sogar einem vollständig separaten Objekt, das vom Protokoll komponiert wird, erreicht werden, aber es könnte auch durch Unterklassifizierung implementiert werden (wie bei Twists LineReceiver). Für die andere Richtung könnte das Protokoll eine write_line Methode haben, die die erforderlichen \r\n hinzufügt und den neuen Byte-Puffer an den Transport weitergibt.
Dieses PEP schlägt eine generalisierte LineReceiver namens ChunkProtocol vor, bei der ein "Chunk" eine Nachricht in einem Stream ist, die durch den angegebenen Begrenzer getrennt ist. Instanzen nehmen einen Begrenzer und einen Callable entgegen, der mit einem Daten-Chunk aufgerufen wird, sobald er empfangen wurde (im Gegensatz zum Unterklassifizierungsverhalten von Twisted). ChunkProtocol verfügt auch über eine write_chunk Methode, die analog zur oben beschriebenen write_line Methode ist.
Warum Protokolle und Transporte trennen?
Diese Trennung zwischen Protokoll und Transport verwirrt oft Leute, die sie zum ersten Mal kennenlernen. Tatsächlich trifft die Standardbibliothek diese Unterscheidung in vielen Fällen nicht, insbesondere nicht in der API, die sie Benutzern zur Verfügung stellt.
Es ist dennoch eine sehr nützliche Unterscheidung. Im schlimmsten Fall vereinfacht sie die Implementierung durch klare Trennung der Zuständigkeiten. Häufiger dient sie jedoch dem weit nützlicheren Zweck, Protokolle über verschiedene Transporte hinweg wiederverwenden zu können.
Betrachten Sie ein einfaches RPC-Protokoll. Die gleichen Bytes können über viele verschiedene Transporte übertragen werden, z. B. Pipes oder Sockets. Um dabei zu helfen, trennen wir das Protokoll vom Transport. Das Protokoll liest und schreibt nur Bytes und kümmert sich nicht wirklich darum, welcher Mechanismus letztendlich verwendet wird, um diese Bytes zu übertragen.
Dies ermöglicht auch das einfache Stapeln oder Verschachteln von Protokollen, was zu noch mehr Code-Wiederverwendung führt. Ein gängiges Beispiel dafür ist JSON-RPC: Gemäß der Spezifikation kann es über Sockets und HTTP verwendet werden [1]. In der Praxis wird es tendenziell hauptsächlich in HTTP gekapselt. Die Protokoll-Transport-Abstraktion ermöglicht es uns, einen Stapel von Protokollen und Transporten zu erstellen, der es Ihnen ermöglicht, HTTP wie einen Transport zu verwenden. Für JSON-RPC könnte dies zu einem Stapel führen, der ungefähr so aussieht
- TCP-Socket-Transport
- HTTP-Protokoll
- HTTP-basierter Transport
- JSON-RPC-Protokoll
- Anwendungscode
Flusskontrolle
Konsumenten
Konsumenten konsumieren Bytes, die von Produzenten erzeugt werden. Zusammen mit Produzenten ermöglichen sie Flusskontrolle.
Konsumenten spielen primär eine passive Rolle bei der Flusskontrolle. Sie werden aufgerufen, wenn ein Produzent Daten verfügbar hat. Sie verarbeiten dann diese Daten und geben die Kontrolle typischerweise an den Produzenten zurück.
Konsumenten implementieren typischerweise irgendeine Art von Puffern. Sie ermöglichen Flusskontrolle, indem sie ihrem Produzenten den aktuellen Status dieser Puffer mitteilen. Ein Konsument kann einen Produzenten anweisen, die Produktion vollständig einzustellen, die Produktion vorübergehend einzustellen oder die Produktion fortzusetzen, wenn er zuvor angewiesen wurde, zu pausieren.
Produzenten werden über die Methode register beim Konsumenten registriert.
Produzenten
Wo Konsumenten Bytes konsumieren, produzieren Produzenten sie.
Produzenten sind an der IPushProducer-Schnittstelle von Twisted angelehnt. Obwohl es auch einen IPullProducer gibt, ist dieser insgesamt weitaus weniger interessant und liegt daher wahrscheinlich außerhalb des Geltungsbereichs dieses PEPs.
Obwohl Produzenten angewiesen werden können, die Produktion vollständig einzustellen, sind die beiden interessantesten Methoden, die sie haben, pause und resume. Diese werden normalerweise vom Konsumenten aufgerufen, um anzuzeigen, ob er bereit ist, mehr Daten zu verarbeiten ("konsumieren") oder nicht. Konsumenten und Produzenten arbeiten zusammen, um Flusskontrolle zu ermöglichen.
Zusätzlich zur Twisted IPushProducer-Schnittstelle haben Produzenten eine Methode half_register, die mit dem Konsumenten aufgerufen wird, wenn der Konsument versucht, diesen Produzenten zu registrieren. In den meisten Fällen handelt es sich lediglich um das Setzen von self.consumer = consumer, aber einige Produzenten erfordern möglicherweise komplexere Vorbedingungen oder Verhaltensweisen, wenn ein Konsument registriert wird. Endbenutzer sollen diese Methode nicht direkt aufrufen.
Betrachtete API-Alternativen
Generatoren als Produzenten
Generatoren wurden als Möglichkeit zur Implementierung von Produzenten vorgeschlagen. Allerdings scheint es hier einige Probleme zu geben.
Erstens gibt es ein konzeptionelles Problem. Ein Generator ist im Grunde "passiv". Er muss durch einen Methodenaufruf aufgefordert werden, Maßnahmen zu ergreifen. Ein Produzent ist "aktiv": Er initiiert diese Methodenaufrufe. Ein echter Produzent hat eine symmetrische Beziehung zu seinem Konsumenten. Im Fall eines als Produzenten umfunktionierten Generators hätte nur der Konsument eine Referenz, und der Produzent wäre sich der Existenz des Konsumenten nicht bewusst.
Dieses konzeptionelle Problem schlägt sich auch in einigen technischen Problemen nieder. Nach einem erfolgreichen Aufruf der write-Methode auf seinem Konsumenten kann ein (Push-)Produzent wieder Maßnahmen ergreifen. Im Fall eines Generators müsste er entweder aufgefordert werden, das nächste Objekt über das Iterationsprotokoll anzufordern (ein Prozess, der unbegrenzt blockieren könnte), oder vielleicht durch Auslösen einer Art Signal-Exception.
Dieses Signal-Setup mag eine technisch machbare Lösung darstellen, ist aber dennoch unbefriedigend. Erstens führt dies zu unnötiger Komplexität im Konsumenten, der nun nicht nur verstehen muss, wie Daten empfangen und verarbeitet werden, sondern auch, wie neue Daten angefordert werden und was zu tun ist, wenn keine neuen Daten verfügbar sind.
Dieser letztere Sonderfall ist besonders problematisch. Er muss berücksichtigt werden, da die gesamte Operation nicht blockieren darf. Generatoren können jedoch keine Ausnahme bei der Iteration auslösen, ohne zu terminieren und dabei den Zustand des Generators zu verlieren. Folglich müsste das Signalieren fehlender Daten über einen Sentinel-Wert erfolgen und nicht über den AusnahCollecting-Mechanismus.
Zu guter Letzt hat niemand tatsächlich funktionierenden Code produziert, der demonstriert, wie sie verwendet werden könnten.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-3153.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT