Zusammenfassung
Dies ist ein asynchroner E/A-Vorschlag für Python3, beginnend mit Python3.3. Studienspezifische Vorschläge fehlen in PEP 3153. Dieser Vorschlag umfasst eine steckbare Ereignisschleifen-API, Transport- und Protokollabstraktionen ähnlich wie Twisted sowie einen ertragsbasierten Scheduler auf höherer Ebene von (PEP 380). Eine Referenzimplementierung in einem Werk, ihr Code heißt Tulip (der Link zum Repo von Tulip befindet sich im Referenzabschnitt am Ende des Artikels).
Einführung
Ereignisschleifen werden häufig an Orten mit hoher Interoperabilität verwendet. Für Frameworks wie Twisted, Tornado oder ZeroMQ (basierend auf Python 3.3) sollte es einfach sein, die Standardimplementierung der Ereignisschleife durch einfache Wrapper oder Proxys entsprechend den Anforderungen des Frameworks anzupassen oder eine eigene Implementierung der Ereignisschleife zu verwenden, um die Standardimplementierung zu ersetzen Durchführung. (Einige Frameworks, wie Twisted, verfügen über mehrere Event-Loop-Implementierungen. Da diese Implementierungen alle über eine einheitliche Schnittstelle verfügen, sollte dies kein Problem darstellen.)
Es kann sogar zu zwei verschiedenen Event-Loop-Interaktionen kommen wird durch die gemeinsame Nutzung der Standard-Ereignisschleife (jeder verwendet seinen eigenen Adapter) oder durch die gemeinsame Nutzung der Ereignisschleife eines der Frameworks implementiert. Im letzteren Fall können zwei unterschiedliche Anpassungsebenen vorhanden sein (von der Ereignisschleife von Framework A zur Standard-Ereignisschleifenschnittstelle und dann vom Standard zur Ereignisschleife von Framework B). Die verwendete Ereignisschleifenimplementierung sollte unter der Kontrolle des Hauptprogramms stehen (obwohl eine Standardstrategie bereitgestellt wird, die die Ereignisschleife auswählen kann).
Daher sind zwei separate APIs definiert:
Abrufen und Festlegen des aktuellen Ereignisschleifenobjekts;
Eine Schnittstelle zum Bestätigen des Ereignisschleifenobjekts und seiner Mindestgarantie
Eine Ereignisschleifenimplementierung kann zusätzliche Methoden und Garantien bereitstellen.
Die Ereignisschleifenschnittstelle ist nicht von der Ausgabe abhängig, sondern verwendet stattdessen eine Kombination aus Rückrufen, zusätzlichen Schnittstellen (Transportprotokolle und Protokolle) und Futures (?). Letzteres ähnelt der in PEP3148 definierten Schnittstelle, verfügt jedoch über eine andere Implementierung und ist nicht an Threads gebunden. Insbesondere verfügen sie nicht über eine wait()-Methode, sondern Benutzer verwenden Rückrufe.
Für diejenigen, die Callback-Funktionen nicht gerne verwenden (einschließlich mir), bietet (Python) einen Scheduler zum Schreiben von asynchronem I/O-Code als Coroutine, der die Ausbeute des Ausdrucks von PEP 380 verwendet. Dieser Planer ist nicht steckbar; die Steckbarkeit besteht auf der Ebene der Ereignisschleife, und der Planer sollte bei jeder standardkonformen Ereignisschleifenimplementierung funktionieren.
Für die Interoperabilität mit Code, der Coroutinen und andere asynchrone Frameworks verwendet, verfügt der Scheduler über eine Task-Klasse, die sich wie eine Zukunft verhält. Ein Framework, das auf Ereignisschleifenebene interagiert, kann einer Zukunft eine Rückruffunktion hinzufügen und auf den Abschluss einer Zukunft warten. Ebenso stellt der Scheduler eine Operation bereit, um die Coroutine anzuhalten, bis die Rückruffunktion aufgerufen wird.
Stellen Sie Einschränkungen für die Interaktion zwischen Threads über die Ereignisschleifenschnittstelle bereit (in Python). Es gibt eine API, die eine Funktion an einen Executor senden kann, der eine mit der Ereignisschleife kompatible Zukunft zurückgeben kann (siehe PEP 3148).
Ohne Zweck
Systeminteroperabilität wie Stackless Python oder Greenlets/Gevent ist nicht der Zweck dieses Tutorials.
Spezifikation
Abhängigkeiten
Python3.3 ist erforderlich. Über den Umfang von Python 3.3 hinaus ist keine neue Sprache oder Standardbibliothek erforderlich. Es sind keine Module oder Pakete von Drittanbietern erforderlich.
Modul-Namespace
Die Spezifikationen hier werden in einem neuen Top-Level-Paket platziert. Verschiedene Komponenten werden in verschiedenen Untermodulen dieses Pakets platziert. Pakete importieren häufig verwendete APIs aus ihren jeweiligen Untermodulen und stellen sie als Paketeigenschaften zur Verfügung (ähnlich wie E-Mail-Pakete erstellt werden).
Der Name des Top-Level-Pakets wurde noch nicht angegeben. Die Referenzimplementierung trägt den Namen „tulip“, aber dieser Name kann in einen etwas ärgerlicheren Namen geändert werden, wenn diese Implementierung zur Standardbibliothek hinzugefügt wird (hoffentlich in Python 3.4).
Bevor lästige Namen gewählt werden, wird in diesem Tutorial „tulip“ als Name des Pakets der obersten Ebene verwendet. Es wird davon ausgegangen, dass auf Klassen und Funktionen ohne angegebenen Modulnamen über das Paket der obersten Ebene zugegriffen wird.
Ereignisschleifenstrategie: Abrufen und Festlegen der Ereignisschleife
Um die aktuelle Ereignisschleife abzurufen, können Sie get_event_loop() verwenden. Diese Funktion gibt eine Instanz der unten definierten EventLoop-Klasse oder ein entsprechendes Objekt zurück. get_event_loop() gibt möglicherweise unterschiedliche Objekte basierend auf dem aktuellen Thread oder unterschiedliche Objekte basierend auf anderen Kontextkonzepten zurück.
Um die aktuelle Ereignisschleife festzulegen, können Sie set_event_loop(event_loop) verwenden, wobei event_loop eine Instanz der EventLoop-Klasse oder ein äquivalentes Instanzobjekt ist. Hier wird das gleiche Kontextkonzept wie bei get_event_loop() verwendet.
Es gibt auch eine dritte Strategiefunktion: new_event_loop(), die für Unit-Tests und andere Sondersituationen nützlich ist. Sie erstellt eine neue EventLoop-Instanz basierend auf den Standardregeln der Strategie und gibt sie zurück. Um es zur aktuellen Ereignisschleife zu machen, müssen Sie set_event_loop() aufrufen.
Um die Funktionsweise der oben genannten drei Funktionen (einschließlich des Konzepts ihres Kontexts) zu ändern, können Sie set_event_loop_policy(policy) aufrufen, wobei der Parameter „policy“ ein Ereignisschleifen-Richtlinienobjekt ist. Dieses Richtlinienobjekt kann jedes Objekt sein, das ähnliche Funktionen wie die oben beschriebenen enthält (get_event_loop(), set_event_loop(event_loop) und new_event_loop()). Die Standard-Ereignisschleifenrichtlinie ist eine Instanz der DefaultEventLoopPolicy-Klasse. Das aktuelle Ereignisschleifen-Richtlinienobjekt kann durch Aufrufen von get_event_loop_policy() abgerufen werden.
Eine Ereignisschleifenstrategie schreibt nicht vor, dass nur eine Ereignisschleife existieren kann. Die standardmäßige Ereignisschleifenrichtlinie erzwingt dies nicht, erzwingt jedoch, dass es nur eine Ereignisschleife pro Thread geben kann.
Ereignisschleifenschnittstelle
Über Zeit: In Python werden alle Zeitüberschreitungen, Intervalle und Verzögerungen in Sekunden berechnet, es kann eine Ganzzahl oder ein Gleitkomma sein Typ. Die Genauigkeit der Uhr hängt von der Implementierung ab; der Standardwert ist time.monotonic().
Über Rückrufe und Handler: Wenn eine Funktion eine Rückruffunktion und eine beliebige Anzahl von Variablen als Parameter akzeptiert, können Sie die Rückruffunktion auch durch ein Handler-Funktionsobjekt (Handler) ersetzen. In diesem Fall ist es nicht erforderlich, diese Parameter zu übergeben. Dieses Handler-Funktionsobjekt sollte eine Funktion sein, die sofort zurückkehrt (fromcall_soon()), nicht eine verzögerte Rückkehr (fromcall_later()). Wenn der Handler abgebrochen wurde, hat dieser Aufruf keine Wirkung.
Ein standardkonformes Ereignisschleifenobjekt verfügt über die folgenden Methoden:
run(). Führen Sie die Ereignisschleife aus, bis nichts mehr zu tun ist. Die spezifische Bedeutung ist:
Außer dem Abbrechen des Anrufs sind keine weiteren Anrufe über call_later(), call_repeatedly(), call_soon() odercall_soon_threadsafe() geplant.
Keine registrierten Dateideskriptoren mehr. Der Dateideskriptor wird von der registrierenden Partei nicht registriert, wenn er geschlossen wird.
Hinweis: run() blockiert, bis es auf eine Beendigungsbedingung stößt oder stop() aufruft.
Hinweis: Wenn Sie call_repeatedly() zum Ausführen eines Aufrufs verwenden, wird run() nicht beendet, bevor Sie stop() aufrufen.
Muss näher erläutert werden: Wie viele ähnliche Maßnahmen müssen wir wirklich durchführen?
run_forever(). Die Ereignisschleife läuft, bis stop() aufgerufen wird.
run_until_complete(future, timeout=None). Die Ereignisschleife läuft, bis die Zukunft abgeschlossen ist. Wenn ein Timeout-Wert angegeben wird, wird auf die Timeout-Zeit gewartet. Wenn der Future abgeschlossen ist, wird sein Ergebnis zurückgegeben oder seine Ausnahme wird ausgelöst; wenn der Future vor dem Timeout abgeschlossen wird oder stop() aufgerufen wird, wird ein TimeoutError ausgelöst (aber der Future wird während der Ereignisschleife nicht abgebrochen). läuft bereits, diese Methode kann nicht aufgerufen werden.
Hinweis: Diese API wird eher für Tests oder ähnliche Arbeiten verwendet. Es sollte nicht als zukünftiger Ersatz für yield from Ausdrücke oder andere Methoden verwendet werden, die auf eine Zukunft warten. (z. B. Registrierung eines Abschlussrückrufs).
run_once(timeout=Keine). Führen Sie eine Ereignisschleife für ein Ereignissegment aus. Wenn ein Timeout-Wert angegeben wird, wird die E/A-Abfrage für einen bestimmten Zeitraum blockiert. Andernfalls ist die E/A-Abfrage nicht zeitgebunden.
Hinweis: Um genau zu sein, wie viel Arbeit hier geleistet wird, hängt von der konkreten Implementierung ab. Eine Einschränkung ist: Wenn man call_soon() verwendet, um sich selbst direkt zu planen, führt dies zu einem Fehler und run_once() wird trotzdem zurückgegeben.
stop(). Stoppen Sie die Ereignisschleife so schnell wie möglich. Die Schleife (oder eine Variante davon) kann dann mit run() neu gestartet werden.
Hinweis: Je nach spezifischer Implementierung gibt es mehrere zu stoppende Blöcke. Alle direkten Rückruffunktionen, die vor stop() ausgeführt wurden, müssen weiterhin ausgeführt werden, aber geplante (oder verzögerte) Rückruffunktionen nach dem Aufruf von stop() werden nicht ausgeführt.
close(). Schließt die Ereignisschleife und gibt alle darin enthaltenen Ressourcen frei, z. B. von epoll() oder kqueue() verwendete Dateideskriptoren. Diese Methode sollte nicht aufgerufen werden, während die Ereignisschleife ausgeführt wird. Es kann mehrfach aufgerufen werden.
call_later(delay, callback, *args). Planen Sie einen Rückruf (*args) mit einer Verzögerung von ca. Verzögerungssekunden, bis er aufgerufen wird, sofern er nicht abgebrochen wird. Geben Sie ein Handler-Objekt zurück, um die Callback-Funktion darzustellen. Die Methode cancel() des Handler-Objekts wird häufig zum Abbrechen der Callback-Funktion verwendet.
call_repeatedly(interval, callback, **args). Ähnlich wie call_later(), aber die Callback-Funktion wird alle Sekunden wiederholt aufgerufen, bis der zurückgegebene Handler abgebrochen wird. Der erste Anruf erfolgt innerhalb von Intervallsekunden.
call_soon(callback, *args). Ähnlich wie call_later(0, callback, *args).
call_soon_threadsafe(callback, *args). Ähnlich wie call_soon(callback, *args), aber wenn die Ereignisschleife blockiert ist und auf E/A wartet und in einem anderen Thread aufgerufen wird, wird die Blockierung der Ereignisschleife aufgehoben. Dies ist die einzige Methode, die sicher von einem anderen Thread aufgerufen werden kann. (Um eine Rückruffunktion mit einer Verzögerung threadsicher zu planen, können Sie ev.call_soon_threadsafe(ev.call_later,when,callback,*args) verwenden.) Es ist jedoch nicht sicher, von einem Signalhandler aus aufzurufen (weil es Schlösser können verwendet werden).
add_signal_handler(sig, callback, *args). Immer wenn das Signal „sigis“ empfangen wird, wird der Aufruf von Callback(*args) geplant. Gibt einen Handler zurück, der zum Abbrechen der Signal-Callback-Funktion verwendet werden kann. (Das Abbrechen des Rückgabehandlers führt dazu, dass „remove_signal_handler()“ aufgerufen wird, wenn das nächste Signal eintrifft. Rufen Sie zuerst „remove_signal_handler()“ explizit auf.) Definieren Sie eine andere Rückgabefunktion für dasselbe Signal, um den vorherigen Handler zu ersetzen (jedes Signal kann nur als Handler aktiviert werden). . Der Sig-Parameter muss ein gültiger Signalwert sein, der im Signalmodul definiert ist. Dadurch wird eine Ausnahme ausgelöst, wenn das Signal nicht verarbeitet werden kann: Wenn es kein gültiges Signal oder ein nicht abfangbares Signal ist (z. B. SIGKILL), wird ein ValueError ausgelöst. Wenn diese bestimmte Ereignisschleifeninstanz das Signal nicht verarbeiten kann (da Signale globale Variablen für jeden Prozessor sind und nur die Ereignisschleife des Hauptthreads diese Signale verarbeiten kann), wird ein RuntimeError ausgelöst.
remove_signal_handler(sig). Handler für Signalsignatur entfernen, wenn festgelegt. Löst die gleiche Ausnahme aus wie add_signal_handler() (mit der Ausnahme, dass False zurückgegeben wird, anstatt einen RuntimeError auszulösen, wenn kein gutes Signal empfangen werden kann). Wenn der Handler erfolgreich entfernt wurde, wird True zurückgegeben. Wenn der Handler nicht festgelegt ist, wird False zurückgegeben.
Einige Methoden, die Standardschnittstellen entsprechen und Future zurückgeben:
wrap_future(future). Dies erfordert die in PEP 3148 beschriebene Zukunft (z. B. eine Instanz von concurrent.futures.Future) und die Rückgabe einer mit der Ereignisschleife kompatiblen Zukunft (z. B. eine Instanz von tulip.Future).
run_in_executor(executor, callback, *args). Sorgen Sie für den Aufruf von callback(*args) in einem Executor (siehe PEP 3148). Das erfolgreiche Ergebnis der zurückgegebenen Zukunft ist der Rückgabewert des Aufrufs. Diese Methode entspricht wrap_future(executor.submit(callback, *args)). Wenn kein Executor vorhanden ist, gibt es einen ThreadPoolExecutor, der standardmäßig 5 Threads verwendet.
set_default_executor(executor) Legt einen Standard-Executor fest, der von run_in_executor() verwendet wird.
getaddrinfo(host, port, family=0, type=0, proto=0, flags=0). Ähnlich wie die Funktion socket.getaddrinfo(), gibt aber eine Zukunft zurück. Das erfolgreiche Ergebnis von Future ist eine Datenspalte im gleichen Format wie der Rückgabewert von socket.getaddrinfo(). Die Standardimplementierung ruft socket.getaddrinfo() über run_in_executor() auf, andere Implementierungen verwenden jedoch möglicherweise ihre eigene DNS-Suche. Optionale Argumente müssen als Schlüsselwortargumente angegeben werden.
getnameinfo(sockaddr, flags=0). Ähnlich wie socket.getnameinfo(), gibt aber eine Zukunft zurück. Das erfolgreiche Ergebnis von Future wird ein Array von (Host, Port) sein. Hat die gleiche Implementierung wie „forgetaddrinfo()“.
create_connection(protocol_factory, host, port, **kwargs) Erstellen Sie eine Streaming-Verbindung mit dem angegebenen Host und Port. Dadurch wird eine transportabhängige Implementierung zur Darstellung der Verbindung erstellt, dann wird Protocol_factory() aufgerufen, um die Protokollimplementierung des Benutzers zu instanziieren (oder abzurufen) und dann die beiden miteinander verknüpft. (Siehe die Definitionen von Transport und Protokoll unten.) Die Protokollimplementierung des Benutzers wird durch Aufrufen von Protocol_factory() ohne Parameter (*) erstellt oder abgerufen. Der Rückgabewert ist ein Future, dessen erfolgreiches Ergebnis das (Transport-, Protokoll-)Paar ist. Wenn ein Fehler die Erstellung einer erfolgreichen Verbindung verhindert hat, enthält der Future einen entsprechenden Ausnahmesatz. Beachten Sie, dass nach Abschluss der Zukunft die Methode „connection_made()“ des Protokolls nicht aufgerufen wird; dies geschieht, wenn der Verbindungs-Handshake abgeschlossen ist.
(*) Es ist nicht erforderlich, dass Protocol_factory eine Klasse ist. Wenn Ihre Protokollklasse die Übergabe definierter Parameter an den Konstruktor erfordert, können Sie Lambda oder functool.partial() verwenden. Sie können auch ein Lambda einer zuvor erstellten Protokollinstanz übergeben.
Optionale Schlüsselparameter:
Familie, Proto, Flags: Adressfamilien-, Protokoll- und gemischte Flag-Parameter werden an getaddrinfo() übergeben. Diese sind alle standardmäßig auf 0 eingestellt. ((Der Socket-Typ ist immer SOCK_STREAM.)
ssl: Übergeben Sie True, um einen SSL-Transport zu erstellen (hergestellt durch Standardeinstellung auf ein einfaches TCP). Oder übergeben Sie ein ssl.SSLConteext-Objekt, um die Standardeinstellung zu überschreiben. Verwenden Sie den SSL-Kontext Objekt.
start_serving(protocol_factory, host, **kwds) Gibt einen Future zurück, der abgeschlossen ist, sobald die Schleife auf „Is None“ gesetzt ist Parameterlos (*) Protocol_factory wird aufgerufen, um ein Protokoll zu erstellen, ein Transport, der die Netzwerkseite der Verbindung darstellt, wird erstellt, und die beiden Objekte werden durch Aufrufen von Protocol.connection_made(transport) (*) miteinander verbunden die ergänzende Beschreibung von create_connection(). Da jedoch Protocol_factory() nur einmal für jede eingehende Verbindung aufgerufen wird, wird empfohlen, bei jedem Aufruf ein neues Protokollobjekt zurückzugeben:
Familie, Proto, Flags: Adressfamilie, Protokoll und gemischte Flag-Parameter, die an getaddrinfo() übergeben werden. ((Der Socket-Typ ist immer SOCK_STREAM.)
Zusätzlich: Wird SSL unterstützt? Ich weiß nicht, wie man (SSL) asynchron unterstützt. Ich schlage vor, dass hierfür ein Zertifikat erforderlich ist.
Hinzugefügt: Vielleicht kann das Ergebnisobjekt einer Zukunft verwendet werden, um die Dienstschleife zu steuern, z. B. um den Dienst zu stoppen, alle aktiven Verbindungen zu beenden und (falls unterstützt) den Rückstand oder andere Parameter anzupassen? Es kann auch über eine API zum Abfragen aktiver Verbindungen verfügen. Wenn die Schleife aufgrund eines Fehlers nicht mehr ausgeführt werden kann oder nicht gestartet werden kann, eine Zukunft (Unterklasse?) zurückgeben, die gerade abgeschlossen wird? Wenn Sie den Vorgang abbrechen, wird die Schleife möglicherweise angehalten.
Zusätzlich: Einige Plattformen sind möglicherweise nicht daran interessiert, alle diese Methoden zu implementieren. Beispielsweise ist die mobile APP nicht sehr an start_serving() interessiert. (Obwohl ich einen Minecraft-Server auf meinem iPad habe...)
Die folgenden Methoden zum Registrieren von Rückruffunktionen für Dateideskriptoren sind nicht erforderlich. Wenn diese Methoden nicht implementiert sind, wird beim Zugriff auf diese Methoden (anstatt sie aufzurufen) ein AttributeError zurückgegeben. Die Standardimplementierung stellt diese Methoden bereit, Benutzer verwenden sie jedoch im Allgemeinen nicht direkt und werden ausschließlich von der Transportschicht verwendet. Ebenso sind diese Methoden auf der Windows-Plattform nicht unbedingt implementiert. Dies hängt davon ab, ob das Select- oder das IOCP-Ereignisschleifenmodell verwendet wird. Beide Modelle akzeptieren einen ganzzahligen Dateideskriptor anstelle des von der Methode fileno() zurückgegebenen Objekts. Der Dateideskriptor ist vorzugsweise abfragbar, eine Festplattendatei beispielsweise nicht.
add_reader(fd, callback, *args) Rufen Sie die angegebene Rückruffunktion callback(*args) auf, wenn der Dateideskriptor fd für Lesevorgänge bereit ist. Gibt ein Handler-Funktionsobjekt zurück, das zum Abbrechen der Rückruffunktion verwendet werden kann. Beachten Sie, dass diese Rückruffunktion im Gegensatz zu call_later() mehrmals aufgerufen werden kann. Ein erneuter Aufruf von add_reader() für denselben Dateideskriptor bricht die zuvor festgelegte Rückruffunktion ab. Hinweis: Der Abbruchhandler kann warten, bis die Handlerfunktion aufgerufen wird. Wenn Sie fd schließen möchten, sollten Sie „remove_reader(fd)“ aufrufen. (TODO: Wenn die Handlerfunktion festgelegt wurde, eine Ausnahme auslösen).
add_writer(fd, callback, *args). Ähnlich wie add_reader(), aber die Callback-Funktion wird aufgerufen, bevor der Schreibvorgang ausgeführt werden kann.
remove_reader(fd). Entfernt die Rückruffunktion für den Lesevorgang, die für den Dateideskriptor fd festgelegt wurde. Wenn die Rückruffunktion nicht festgelegt ist, wird keine Operation ausgeführt. (Eine solche alternative Schnittstelle wird bereitgestellt, da das Aufzeichnen von Dateideskriptoren praktischer und einfacher ist als das Aufzeichnen von Handlerfunktionen.) Gibt True zurück, wenn der Löschvorgang erfolgreich war, und False, wenn er fehlschlägt.
remove_writer(fd). Entfernt die Rückruffunktion für den Schreibvorgang, die für den Dateideskriptor fd festgelegt wurde.
Unvollendet: Was ist zu tun, wenn ein Dateideskriptor mehrere Rückruffunktionen enthält? Der aktuelle Mechanismus besteht darin, die vorherige Rückruffunktion zu ersetzen. Wenn die Rückruffunktion registriert wurde, sollte eine Ausnahme ausgelöst werden.
Die folgenden Methoden sind bei der asynchronen Socket-E/A optional. Sie stellen eine Alternative zu den oben genannten optionalen Methoden dar und sollen mit IOCP in der Transportimplementierung von Windows verwendet werden (sofern die Ereignisschleife dies unterstützt). Der Socket-Parameter darf den Socket nicht blockieren.
sock_recv(sock, n). Empfangen Sie Bytes vom Socket-Sock. Gibt einen Future zurück, der bei Erfolg ein Byte-Objekt ist.
sock_sendall(sock, data). Senden Sie Datenbytes an den Socket-Sock. Gibt eine Zukunft zurück. Das Ergebnis von Zukunft wird nach Erfolg None sein. (Zusätzlich: Wäre es besser, sendall() oder send() emulieren zu lassen? Aber ich denke, sendall() – vielleicht sollte es send() heißen?)
sock_connect(sock, address) . Stellen Sie eine Verbindung zur angegebenen Adresse her. Gibt eine Zukunft zurück, das erfolgreiche Ergebnis von Zukunft ist None.
sock_accept(sock). Erhalten Sie einen Link vom Socket. Der Socket muss sich im Überwachungsmodus befinden und an einen benutzerdefinierten Socket gebunden sein. Gibt einen Future zurück. Das erfolgreiche Ergebnis des Future ist ein Array von (conn, peer), wobei conn ein verbundener nicht blockierender Socket und peer die Peer-Adresse ist. (Seitenleiste: Die Leute sagen mir, dass dieser API-Stil für High-Level-Server sehr langsam ist. Daher gibt es oben start_sering(). Brauchen wir das noch?)
Seitenleiste: Keine der optionalen Methoden ist sehr gut. Vielleicht werden diese alle benötigt? Es kommt weiterhin auf eine effizientere Einrichtung der Plattform an. Eine andere Möglichkeit besteht darin, dass in der Dokumentation darauf hingewiesen wird, dass diese „nur zur Übertragung verfügbar“ sind und andere „auf jeden Fall verfügbar“.
Rückrufreihenfolge
Wenn zwei Rückruffunktionen gleichzeitig geplant sind, werden sie in der Reihenfolge ausgeführt, in der sie registriert wurden. Zum Beispiel:
ev.call_soon(foo)
ev.call_soon(bar)
garantiert, dass foo() in bar() ausgeführt wird.
Wenn call_soon() verwendet wird, gilt diese Garantie auch dann noch, wenn die Systemuhr rückwärts geht. Dies funktioniert auch für call_later(0,callback,*args). Wenn call_later() jedoch ohne Verzögerung verwendet wird, während die Systemuhr rückwärts läuft, gibt es keine Garantie. (Eine gute Implementierung einer Ereignisschleife sollte time.monotonic() verwenden, um Probleme zu vermeiden, die durch rückläufige Situationen der Systemuhr verursacht werden. Siehe PEP 418.)
Kontext
Alle Ereignisschleifen haben das Konzept des Kontexts. Für die Standardimplementierung der Ereignisschleife ist der Kontext ein Thread. Eine Ereignisschleifenimplementierung sollte alle Rückrufe im selben Kontext ausführen. Eine Ereignisschleifenimplementierung sollte jeweils nur einen Rückruf ausführen, daher ist der Rückruf dafür verantwortlich, den automatischen gegenseitigen Ausschluss von anderen Rückrufen aufrechtzuerhalten, die in derselben Ereignisschleife geplant sind.
Ausnahme
Es gibt zwei Arten von Ausnahmen in Python: solche, die von der Exception-Klasse abgeleitet sind, und solche, die von BaseException abgeleitet sind. Von Exception abgeleitete Ausnahmen werden normalerweise abgefangen und entsprechend behandelt. Ausnahmen werden beispielsweise über einen Future weitergeleitet und wenn sie innerhalb eines Rückrufs auftreten, werden sie protokolliert und ignoriert.
Ausnahmen von BaseException werden jedoch nie abgefangen; sie führen normalerweise zu einem Fehler-Traceback und führen zum Beenden des Programms. (Beispiele hierfür sind KeyboardInterrupt und SystemExit; es wäre unklug, diese Ausnahmen wie die meisten anderen Ausnahmen zu behandeln.)
Handler-Klasse
Es gibt verschiedene Methoden zum Registrieren von Rückruffunktionen (z. B. call_later()), die ein Objekt zur Darstellung der Registrierung zurückgeben Das Objekt kann zum Abbrechen der Rückruffunktion verwendet werden. Obwohl Benutzer diese Klasse nie instanziieren müssen, möchten sie diesem Objekt dennoch einen guten Namen geben: Handler. Diese Klasse verfügt über eine öffentliche Methode:
cancel() Versuchen Sie, die Rückruffunktion abzubrechen. Ergänzung: Genaue Angaben.
Schreibgeschützte öffentliche Eigenschaften:
Rückruf. Die aufzurufende Rückruffunktion.
argumente. Array von Parametern zum Aufrufen der Callback-Funktion.
abgesagt. Wenn die Tabelle cancel() aufgerufen wird, ist ihr Wert True.
Es ist zu beachten, dass einige Rückruffunktionen (z. B. die über call_later() registrierten) nur einmal aufgerufen werden dürfen. Andere (z. B. die über add_reader() registrierten) sollen mehrmals aufgerufen werden.
Ergänzung: Eine API zum Aufrufen von Callback-Funktionen (ist es notwendig, die Ausnahmebehandlung zu kapseln)? Muss es aufzeichnen, wie oft es aufgerufen wurde? Vielleicht sollte diese API wie _call_() aussehen? (Aber es sollte die Ausnahme unterdrücken.)
Zusätzlich: Gibt es einige öffentliche Eigenschaften, um die Echtzeitwerte aufzuzeichnen, wenn die Rückruffunktion geplant ist? (Da dies eine Möglichkeit erfordert, es auf dem Heap zu speichern.)
Futures
ulip.Future ist speziell für die Parallelität mit PEP 3148 .futures konzipiert .Future ist ähnlich, mit geringfügigen Unterschieden. Wann immer Future in diesem PEP erwähnt wird, bezieht es sich auf tulip.Future, sofern nicht ausdrücklich als concurrent.futures.Future angegeben. Die von tulip.Future unterstützte öffentliche API lautet wie folgt, und es wird auch auf den Unterschied zu PEP 3148 hingewiesen:
cancel() Wenn die Zukunft abgeschlossen (oder abgebrochen) wurde, wird False zurückgegeben. Andernfalls ändern Sie den Status der Zukunft in den Status „Abgebrochen“ (der auch als abgeschlossen verstanden werden kann), planen Sie die Rückruffunktion und geben Sie „True“ zurück.
cancelled() Gibt True zurück, wenn die Zukunft abgebrochen wurde.
running() gibt immer False zurück. Im Gegensatz zu PEP 3148 gibt es keinen Betriebsstatus.
done() Gibt True zurück, wenn die Zukunft abgeschlossen wurde. Hinweis: Auch ein stornierter Future gilt als abgeschlossen (dies gilt hier und anderswo).
result() Gibt das von set_result() festgelegte Ergebnis oder die von set_Exception() festgelegte Ausnahme zurück. Wenn es abgebrochen wurde, wird ein CancelledError ausgelöst. Im Gegensatz zu PEP 3148 gibt es keinen Timeout-Parameter und keine Wartezeit. Wenn die Zukunft noch nicht abgeschlossen ist, wird eine Ausnahme ausgelöst.
Exception(). Wie oben, es wird eine Ausnahme zurückgegeben.
add_done_callback(fn) Fügen Sie eine Rückruffunktion hinzu, die ausgeführt wird, wenn die Zukunft abgeschlossen (oder abgebrochen) ist. Wenn die Zukunft abgeschlossen ist (oder abgebrochen wurde), wird call_soon() verwendet, um die Rückruffunktion auszulösen. Im Gegensatz zu PEP 3148 werden hinzugefügte Rückruffunktionen nicht sofort aufgerufen und immer im Kontext des Aufrufers ausgeführt. (Normalerweise ist ein Kontext ein Thread). Sie können es so verstehen, dass Sie call_soon() verwenden, um die Rückruffunktion aufzurufen. Hinweis: Die hinzugefügte Rückruffunktion (anders als andere Rückruffunktionen in diesem PEP und ohne Berücksichtigung der Konventionen im Abschnitt „Rückrufstil“ unten) erhält immer einen Future als Parameter und diese Rückruffunktion sollte kein Handler-Objekt sein.
set_result(result). Diese Zukunft darf sich nicht im Abschluss- (oder Abbruch-) Zustand befinden. Diese Methode versetzt die aktuelle Zukunft in den Abschlussstatus und bereitet den Aufruf der entsprechenden Rückruffunktion vor. Im Gegensatz zu PEP 3148: Dies ist eine öffentliche API.
set_Exception(Exception). Dasselbe wie oben, Ausnahme festlegen.
Die interne Methode set_running_or_notify_cancel() wird nicht mehr unterstützt; es gibt keine Möglichkeit, sie direkt in den laufenden Zustand zu versetzen.
Dieses PEP definiert die folgende Ausnahme:
InvalidStateError Diese Ausnahme wird ausgelöst, wenn die aufgerufene Methode den Status dieser Zukunft nicht akzeptiert (zum Beispiel: in einer abgeschlossenen Zukunft rufen Sie set_result( auf. )-Methode oder rufen Sie die result()-Methode für einen unvollendeten Future auf).
InvalidTimeoutError Diese Ausnahme wird ausgelöst, wenn beim Aufruf von result() oder Ausnahme() ein Argument ungleich Null übergeben wird.
CancelledError. Alias von concurrent.futures.CancelledError. Diese Ausnahme wird ausgelöst, wenn die Methode „result()“ oder „Exception()“ für einen abgebrochenen Future aufgerufen wird.
TimeoutError Alias für concurrent.futures.TimeoutError. Kann von der Methode EventLoop.run_until_complete() ausgelöst werden.
Beim Erstellen eines Futures wird dieser mit der Standard-Ereignisschleife verknüpft. (Noch zu erledigen: Übergabe einer Ereignisschleife als Argument zulassen?)
Die Methoden wait() und as_completed() im Paket concurrent.futures akzeptieren keine tulip.Future-Objekte als Parameter. Es gibt jedoch ähnliche APIs tulip.wait() und tulip.as_completed(), die unten beschrieben werden.
Tulip.Future-Objekte können angewendet werden, um Ausdrücke in Unterroutinen (Coroutinen) zu generieren. Dies wird über die Schnittstelle __iter__() in Future implementiert. Weitere Informationen finden Sie im Abschnitt „Unterprogramme und Scheduler“ weiter unten.
Wenn das Future-Objekt wiederverwendet wird und eine zugehörige Ausnahme vorhanden ist, die Methode „result()“, „Exception()“ oder „__iter__()“ jedoch nicht aufgerufen wird (oder eine Ausnahme generiert, aber noch nicht ausgelöst wurde), Dann sollte diese Ausnahme protokolliert werden. TBD: Auf welcher Ebene wird es aufgezeichnet?
In Zukunft werden wir möglicherweise tulip.Future und concurrent.futures.Future vereinen. Fügen Sie beispielsweise dem letztgenannten Objekt eine __iter__()-Methode hinzu, um die Ausbeute von Ausdrücken zu unterstützen. Um zu verhindern, dass die Ereignisschleife versehentlich das unvollendete Ergebnis () aufruft, muss der Blockierungsmechanismus erkennen, ob im aktuellen Thread eine aktive Ereignisschleife vorhanden ist. Andernfalls wird eine Ausnahme ausgelöst. Dieses PEP zielt jedoch darauf ab, externe Abhängigkeiten zu minimieren (basiert nur auf Python3.3), sodass zu diesem Zeitpunkt keine Änderungen an concurrent.futures.Future vorgenommen werden.
Transportschicht
Die Transportschicht bezieht sich auf eine Abstraktionsschicht, die auf Sockets oder anderen ähnlichen Mechanismen (z. B. Pipes oder SSL-Verbindungen) basiert. Die Transportschicht wird hier stark von Twisted und PEP 3153 beeinflusst. Benutzer implementieren oder instanziieren die Transportschicht selten direkt. Die Ereignisschleife stellt entsprechende Methoden zum Festlegen der Transportschicht bereit.
Die Transportschicht wird zum Arbeiten mit Protokollen verwendet. Typische Protokolle kümmern sich nicht um die spezifischen Details der zugrunde liegenden Transportschicht, und die Transportschicht kann für die Arbeit mit einer Vielzahl von Protokollen verwendet werden. Beispielsweise kann eine HTTP-Client-Implementierung eine normale Socket-Transportschicht oder eine SSL-Transportschicht verwenden. Die einfache Socket-Transportschicht kann mit einer großen Anzahl von Protokollen außerhalb des HTTP-Protokolls arbeiten (z. B. SMTP, IMAP, POP, FTP, IRC, SPDY).
Die meisten Verbindungen sind asymmetrischer Natur: Client und Server haben normalerweise unterschiedliche Rollen und Verhaltensweisen. Daher ist auch die Schnittstelle zwischen der Transportschicht und dem Protokoll asymmetrisch. Aus Protokollsicht erfolgt das Senden von Daten durch Aufrufen der write()-Methode des Transportschichtobjekts. Die Methode write() kehrt sofort zurück, nachdem die Daten in den Puffer gestellt wurden. Beim Lesen von Daten spielt die Transportschicht eine aktivere Rolle: Nach dem Empfang von Daten vom Socket (oder einer anderen Datenquelle) ruft die Transportschicht die Methode data_received() des Protokolls auf.
Die Transportschicht verfügt über die folgenden öffentlichen Methoden:
write(data). Der Parameter muss ein Byte-Objekt sein. Gibt „Keine“ zurück. Der Transportschicht steht es frei, Datenbytes zwischenzuspeichern, sie muss jedoch sicherstellen, dass die Daten an das andere Ende gesendet werden und das Verhalten des Datenflusses beibehalten wird. Das heißt: t.write(b'abc'); t.write(b'def') ist äquivalent zu t.write(b'abcdef'), auch äquivalent zu:
t.write(b 'a')
t.write(b'b')
t.write(b'c')
t.write(b'd')
t.write(b'e')
t.write(b'f')
writelines(iterable). data in iterable:
self.write(data)
write_eof() Schließen Sie die Schreibdatenverbindung und die Methode write() darf nicht mehr aufgerufen werden. Wenn alle gepufferten Daten übertragen wurden, signalisiert die Transportschicht dem anderen Ende, dass keine Daten mehr vorhanden sind. Einige Protokolle unterstützen dies nicht; in diesem Fall löst der Aufruf von write_eof() eine Ausnahme aus. (Hinweis: Diese Methode hieß früher half_close(). Sofern Sie die spezifische Bedeutung nicht kennen, gibt dieser Methodenname nicht eindeutig an, welches Ende geschlossen wird.)
can_write_eof() Wenn das Protokoll write_eof() unterstützt. , gibt True zurück; andernfalls wird False zurückgegeben. (Wenn write_eof() nicht verfügbar ist, müssen einige Protokolle das entsprechende Verhalten ändern, daher ist diese Methode erforderlich. Beispielsweise wird in HTTP zum Senden von Daten unbekannter aktueller Größe normalerweise write_eof() verwendet, um anzuzeigen, dass die Daten wurden jedoch nicht gesendet. Um dieses Verhalten zu unterstützen, muss die entsprechende HTTP-Protokollimplementierung die Chunked-Verschlüsselung verwenden. Wenn die Datengröße zum Zeitpunkt des Sendens jedoch unbekannt ist, ist die Verwendung von Content-Length-Header )
pause(). Pausiert das Senden von Daten und ruft direkt die Methode „resume()“ auf. Zwischen dem pause()-Aufruf und dem resume()-Aufruf wird die data_received()-Methode des Protokolls nicht erneut aufgerufen. Nicht gültig in der write()-Methode.
resume() setzt die Datenübertragung mit data_received() des Protokolls fort.
close() Schließt die Verbindung. Die Verbindung wird erst geschlossen, wenn alle mit write() zwischengespeicherten Daten gesendet wurden. Nachdem die Verbindung geschlossen wurde, wird die data_received()-Methode des Protokolls nicht mehr aufgerufen. Wenn alle gepufferten Daten gesendet wurden, wird die Methode „connection_lost()“ des Protokolls mit „None“ als Parameter aufgerufen. Hinweis: Diese Methode garantiert nicht, dass alle oben genannten Methoden aufgerufen werden.
abort(). Die Verbindung abbrechen. Alle noch nicht übertragenen Daten im Puffer werden verworfen. Kurz darauf wird die Funktion „connection_lost()“ des Protokolls aufgerufen und ein None-Parameter übergeben. (Zu bestimmen: Übergeben Sie in close(), abort() oder der Abschlussaktion des anderen Endes verschiedene Parameter an connect_lost()? Oder fügen Sie eine Methode speziell hinzu, um dies abzufragen? Glyph empfiehlt die Übergabe verschiedener Ausnahmen)
Noch nicht abgeschlossen: Stellen Sie eine weitere Methode zur Flusskontrolle bereit: Die Transportschicht kann die Protokollschicht anhalten, wenn die Pufferdaten zu einer Belastung werden. Empfehlung: Wenn das Protokoll über die Methoden „pause()“ und „resume()“ verfügt, erlauben Sie der Transportschicht, diese aufzurufen. Wenn sie nicht vorhanden sind, unterstützt das Protokoll keine Flusskontrolle. (Für pause() und resume() wäre es vielleicht besser, unterschiedliche Namen für die Protokollschicht und die Transportschicht zu verwenden?)
Protokolle
Das Protokoll wird normalerweise in Verbindung mit der Transportschicht verwendet. Hier werden mehrere häufig verwendete Protokolle bereitgestellt (z. B. mehrere nützliche HTTP-Client- und Server-Implementierungen), für deren Implementierung größtenteils Benutzer- oder Drittanbieterbibliotheken erforderlich sind.
Ein Protokoll muss die folgenden Methoden implementieren, die von der Transportschicht aufgerufen werden. Diese Rückruffunktionen werden von der Ereignisschleife im richtigen Kontext aufgerufen (siehe Abschnitt „Kontext“ oben).
connection_made(transport) Bedeutet, dass die Transportschicht bereit und mit dem anderen Ende einer Implementierung verbunden ist. Das Protokoll sollte die Transportschichtreferenz als Variable speichern (damit seine write()-Methode und andere Methoden später aufgerufen werden können) und zu diesem Zeitpunkt auch Handshake-Anfragen senden können.
data_received(data). Die Transportschicht hat einen Teil der Daten gelesen. Der Parameter ist ein Byte-Objekt, das nicht leer ist. Für die Größe dieses Parameters gibt es keine explizite Begrenzung. p.data_received(b'abcdef') sollte der folgenden Anweisung entsprechen:
p.data_received(b'abc')
p.data_received(b'def')
eof_received(). Diese Methode wird aufgerufen, wenn write_eof() oder andere gleichwertige Methoden am anderen Ende aufgerufen werden. Die Standardimplementierung ruft die Methode close() der Transportschicht auf, die die Methode connect_lost() im Protokoll aufruft.
connection_lost(exc). Die Transportschicht wurde geschlossen oder unterbrochen, das andere Ende hat die Verbindung sicher geschlossen oder es ist eine Ausnahme aufgetreten. In den ersten drei Fällen ist der Parameter None; im Falle einer Ausnahme ist der Parameter die Ausnahme, die die Unterbrechung der Transportschicht verursacht hat. (Zu klären: Müssen die ersten drei Situationen unterschiedlich behandelt werden?)
Hier ist ein Diagramm, das die Reihenfolge und Vielfalt der Anrufe zeigt:
connection_made()-- genau einmal
data_received()-- null oder mehrmals
eof_received()-- höchstens einmal
connection_lost()-- genau einmal
Zusätzlich: Benutzer besprechen Code Sollte etwas getan werden, um sicherzustellen, dass Protokolle und Transportprotokolle nicht vorzeitig GCed (Garbage Collection) werden.
Callback-Funktionsstil
Die meisten Schnittstellen akzeptieren Callback-Funktionen und auch Positionsparameter. Um beispielsweise den sofortigen Aufruf von foor("abc",42) zu planen, würden Sie ev.call_soon(foo,"abc",42) aufrufen. Um einen Aufruf von foo() zu planen, verwenden Sie ev.call_soon(foo). Diese Konvention reduziert die Anzahl der kleinen Lambda-Ausdrücke, die für die typische Programmierung von Rückruffunktionen erforderlich sind, erheblich.
Diese Konvention unterstützt ausdrücklich keine Schlüsselwortargumente. Schlüsselwortargumente werden häufig verwendet, um optionale Zusatzinformationen zur Callback-Funktion zu übergeben. Dies ermöglicht elegante Änderungen an der API, ohne sich Gedanken darüber machen zu müssen, ob ein Schlüsselwort irgendwo von einem Aufrufer deklariert wird. Wenn Sie eine Rückruffunktion haben, die mit Schlüsselwortargumenten aufgerufen werden muss, können Sie Lambda-Ausdrücke oder functools.partial verwenden. Zum Beispiel:
ev.call_soon(functools.partial(foo, "abc", repeat=42))
Wählen Sie eine Ereignisschleifenimplementierung
Noch auszufüllen. (Informationen zur Verwendung von select/poll/epoll und zum Ändern der Auswahl. Es gehört zur Ereignisschleifenstrategie)
Coroutinen und Scheduler
Das ist ein unabhängiger Teil der obersten Ebene, da sich sein Status von der Ereignisschleifenschnittstelle unterscheidet. Coroutinen sind optional und es empfiehlt sich, Code einfach mithilfe von Rückrufen zu schreiben. Andererseits gibt es nur eine Scheduler-/Coroutine-API-Implementierung, und wenn Sie sich für Coroutinen entscheiden, ist dies die einzige, die Sie verwenden.
Coroutinen
Eine Coroutine ist ein Produzent, der die folgenden Konventionen befolgt. Aus Gründen einer guten Dokumentation sollten alle Coroutinen mit @tulip.coroutine dekoriert werden, dies ist jedoch nicht unbedingt erforderlich.
Coroutinen verwenden die in PEP 380 eingeführte yield from-Syntax anstelle der ursprünglichen yield-Syntax.
Das Wort „Coroutine“ hat eine ähnliche Bedeutung wie „Produzent“ und wird zur Beschreibung zweier unterschiedlicher (wenn auch verwandter) Konzepte verwendet.
definiert die Coroutine-Funktion (eine Funktion, die mithilfe der Modifikation tulip.coroutine definiert wird). Zur Klarstellung können wir diese Funktion eine Coroutine-Funktion nennen.
Erhalten Sie das Objekt durch eine kollaborative Funktion. Dieses Objekt stellt eine Berechnung oder einen E/A-Vorgang (normalerweise beides) dar, der schließlich abgeschlossen wird. Wir können dieses Objekt ein Coroutine-Objekt nennen, um Mehrdeutigkeiten zu beseitigen.
Was die Coroutine tun kann:
Ergebnis = Ertrag aus der Zukunft verwenden – bis die Zukunft abgeschlossen ist, die Coroutine anhalten und dann zurückgeben Ergebnis der Zukunft oder werfen Sie die Ausnahme aus, die übergeben werden soll.
Ergebnis = Ertrag aus Coroutine verwenden – warten, bis eine andere Coroutine ein Ergebnis liefert (oder eine zu übergebende Ausnahme auslösen). Die Ausnahme dieser Coroutine muss ein Aufruf einer anderen Coroutine sein.
Ergebnis zurückgeben – Gibt ein Ergebnis für die Coroutine zurück, die den Yield-from-Ausdruck verwendet, um auf das Ergebnis zu warten.
Wirft eine Ausnahme aus – löst eine Ausnahme in der Coroutine für (Programm-)Warten aus, indem der Yield-from-Ausdruck verwendet wird.
Der Aufruf einer Coroutine führt ihren Code nicht sofort aus – sie ist lediglich ein Produzent, und die vom Aufruf zurückgegebene Coroutine ist in der Tat nur ein Produzentenobjekt, das nichts tut, bis Sie darüber iterieren. Für eine Coroutine gibt es zwei grundlegende Möglichkeiten, sie zum Laufen zu bringen: Rufen Sie yield von einer anderen Coroutine aus auf (vorausgesetzt, die andere Coroutine läuft bereits!), oder konvertieren Sie sie in eine Task (siehe unten).
Coroutinen können nur ausgeführt werden, wenn die Ereignisschleife ausgeführt wird.
Warten auf mehrere Coroutinen
Es gibt zwei ähnliche wait() und as_completed(), die API im Paket concurrent.futures bietet das Warten auf mehrere Coroutinen Eine Coroutine oder Zukunft:
tulip.wait(fs, timeout=None, return_when=ALL_COMPLETED). Dies ist eine von fs bereitgestellte Coroutine, die auf den Abschluss von Future oder anderen Coroutinen wartet. Coroutine-Parameter werden in Task gekapselt (siehe unten). Diese Methode gibt einen Future zurück, der zwei Future-Sets enthält (done, pending done ist ein ursprünglicher Future (oder gekapselter Coroutine-Set) und pending bedeutet Rest). wie z. B. noch nicht abgeschlossen (oder Stornierung). Die optionalen Parameter timeout und return_when haben die gleiche Bedeutung und den gleichen Standardwert wie die Parameter in concurrent.futures.wait(): timeout, wenn nicht None, gibt ein Timeout für alle Vorgänge an, return_when, gibt an, wann gestoppt werden soll. Die Konstanten FIRST_COMPLETED, FIRST_EXCEPTION, ALL_COMPLETED verwenden dieselbe Wertdefinition und haben in PEP 3148 dieselbe Bedeutung:
ALL_COMPLETED(Standard): Warten, bis alle Futures verarbeitet oder abgeschlossen sind (oder bis ein Timeout auftritt).
FIRST_COMPLETED: Warten Sie, bis mindestens ein Future abgeschlossen oder abgebrochen ist (oder bis ein Timeout auftritt).
FIRST_EXCEPTION: Warten Sie, bis mindestens ein Future aufgrund einer Ausnahme bereit (nicht storniert) ist (es ist magisch, stornierte Futures vom Filter auszuschließen, aber PEP 3148 macht es auf diese Weise)
tulip.as_completed(fs, timeout=None). Gibt einen Iterator mit dem Wert Future zurück; wartet auf einen erfolgreichen Wert, bis der nächste Future oder die nächste Coroutine von fs abgeschlossen wird, während er sein eigenes Ergebnis zurückgibt (oder seine Ausnahme auslöst). . Der optionale Parameter timeout hat die gleiche Bedeutung und den gleichen Standardwert wie die Parameter in concurrent.futures.wait(): Wenn es einen Timeout gibt, löst der nächste vom Iterator zurückgegebene Future während des Wartens eine TimeoutError-Ausnahme aus. Beispiele verwenden:
für f in as_completed(fs):
result = yield from f # Kann eine Ausnahme auslösen.
# Ergebnis verwenden.
Task
Task ist ein Objekt, das unabhängig laufende Unterprogramme verwaltet. Die Task-Schnittstelle und die Future-Schnittstelle sind identisch. Wenn die Unterroutine abgeschlossen ist oder eine Ausnahme auslöst, wird auch die damit verbundene Aufgabe abgeschlossen. Das zurückgegebene Ergebnis ist das Ergebnis der entsprechenden Aufgabe, und die ausgelöste Ausnahme ist die Ausnahme der entsprechenden Aufgabe.
Wenn Sie eine nicht abgeschlossene Aufgabe abbrechen, wird die weitere Ausführung der zugehörigen Unterroutine verhindert. In diesem Fall erhält das Unterprogramm eine Ausnahme, um den Abbruchbefehl besser verarbeiten zu können. Natürlich muss das Unterprogramm diese Ausnahme nicht behandeln. Dieser Mechanismus wird durch den Aufruf der Standardmethode close() des Generators erreicht, die in PEP 342 beschrieben wird.
Aufgaben sind auch für Interaktionen zwischen Unterroutinen und Callback-basierten Frameworks wie Twisted nützlich. Nachdem Sie eine Unterroutine in eine Aufgabe umgewandelt haben, können Sie die Rückruffunktion zur Aufgabe hinzufügen.
Sie fragen sich vielleicht, warum nicht alle Unterprogramme in Aufgaben umwandeln? @tulip.coroutinedecorator kann diese Aufgabe erfüllen. Wenn in diesem Fall ein Unterprogramm ein anderes Unterprogramm aufruft (oder in anderen komplexen Situationen), wird das gesamte Programm ziemlich langsam. In komplexen Situationen ist es viel schneller, Unterprogramme beizubehalten, als sie in Aufgaben umzuwandeln.
Scheduler
Der Scheduler hat keine öffentliche Schnittstelle. Sie können Zukunfts- und Aufgabenerträge verwenden, um mit dem Planer zu interagieren. Tatsächlich verfügt der Scheduler nicht über eine spezifische Klassenimplementierung. Die Klassen Future und Task stellen das Verhalten des Schedulers mithilfe der öffentlichen Schnittstelle der Ereignisschleife dar. Selbst wenn Sie also zu einer Event-Loop-Implementierung eines Drittanbieters wechseln, kann die Scheduler-Funktion weiterhin verwendet werden.
Ruhezustand
Unvollendet: Ruhezustand (Sekunden) Sie können den Ruhezustand (0) zum Anhalten und Abfragen von E/A verwenden.
Coroutinen und Protokolle
Der beste Weg, Coroutinen zum Implementieren von Protokollen zu verwenden, ist die Verwendung eines Stream-Cache, der data_received() verwendet. Füllen Sie die Daten aus, Gleichzeitig können Sie Methoden wie read(n) und readline() verwenden, die einen Future zurückgeben, um die Daten asynchron zu lesen. Wenn die Verbindung geschlossen wird, sollte die Methode read() einen Future zurückgeben, dessen Ergebnis „“ sein wird, oder eine Ausnahme auslösen, wenn „connection_closed()“ aufgrund einer Ausnahme aufgerufen wird.
Zum Schreiben von Daten können die write()-Methoden (und ähnliche) auf dem Transport verwendet werden – diese geben kein Future zurück. Zum Einrichten und Starten von Coroutinen beim Aufruf von Connection_made() sollte eine Standardprotokollimplementierung bereitgestellt werden.
Hinzugefügt: Weitere Spezifikationen.
Abbrechen
Ergänzend. Wenn eine Aufgabe abgebrochen wird, erkennt ihre Coroutine die Ausnahme überall dort, wo sie vom Scheduler abgebrochen wird (z. B. wenn sie möglicherweise mitten in einem Vorgang abbricht). Wir müssen klarstellen, welche Ausnahme ausgelöst werden soll.
Erneut hinzugefügt: Timeout.
Bekannte Probleme
Debugging-API? Zum Beispiel etwas, das viele Dinge aufzeichnet oder ungewöhnliche Bedingungen aufzeichnet (z. B. eine Warteschlange, die sich schneller füllt als leert) oder sogar eine Rückruffunktion, die viel Zeit in Anspruch nimmt ...
Brauchen wir eine introspektive API? Beispielsweise gibt die Rückruffunktion „Request Read“ einen Dateideskriptor zurück. Oder wenn die nächste geplante (Rückruffunktion) aufgerufen wird. Oder einige von der Rückruffunktion registrierte Dateideskriptoren.
Transporte erfordern möglicherweise eine Methode, die versucht, die Adresse des Sockets zurückzugeben (und eine andere Methode, die die entsprechende Adresse zurückgibt). Obwohl dies von der Art des Sockets abhängt, handelt es sich nicht immer um einen Socket. Dann sollte „None“ zurückgegeben werden. (Alternativ könnte es eine Methode geben, die den Socket selbst zurückgibt – aber es ist denkbar, dass man den Socket nicht verwendet, um den IP-Link zu implementieren. Was sollte sie also tun?)
Muss os.fokd() verarbeiten. (Dies könnte im Fall von Tulip bis zur Selektorklasse reichen.)
Vielleicht benötigt start_serving() eine Methode, um einen aktuellen Socket zu übergeben (Gunicorn benötigt diese beispielsweise). create_connection() hat auch das gleiche Problem.
Wir können einige explizite Sperren einführen, obwohl die Verwendung etwas mühsam ist, da wir die Blockierungssyntax with lock: nicht verwenden können (da wir zum Warten auf eine Sperre „Yield from“ verwenden müssen, was die ist). with-Anweisung ist nicht möglich).
Gibt an, ob das Datagramm-Protokoll und der Link unterstützt werden sollen. Möglicherweise gibt es weitere Socket-I/O-Methoden, wie zum Beispiel sock_sendto() und sock_recvfrom(). Oder Benutzer-Clients schreiben ihre eigenen (das ist kein Hexenwerk). Gibt es einen Grund, write(), writelines(), data_received() in einem einzelnen Datagramm zu überschreiben? (Glyph empfiehlt Letzteres.) Was sollte man dann anstelle von write() verwenden? Müssen wir schließlich das verbindungslose Datagrammprotokoll unterstützen? (Das bedeutet, sendto() und recvfrom() zu kapseln.)
Möglicherweise benötigen wir APIs, um verschiedene Supermärkte zu steuern. Beispielsweise möchten wir möglicherweise die Zeit begrenzen, die für die Auflösung von DNS, Verbindungen, SSL-Handshakes, inaktiven Verbindungen oder sogar für jede Sitzung aufgewendet wird. Vielleicht gibt es eine Möglichkeit, das Schlüsselwortargument „timeout“ vollständig zu einigen Methoden hinzuzufügen und andere dazu, Timeouts geschickt über call_later und Task.cancel() zu implementieren. Es kann jedoch Methoden geben, die ein Standard-Timeout erfordern, und wir möchten möglicherweise den Standardwert für globale Vorgänge dieser Spezifikation ändern. (z. B. jede Ereignisschleife).
Ein Ereignisauslöser im NodeJS-Stil? Oder daraus ein separates Tutorial machen? Dies ist im Benutzerbereich tatsächlich recht einfach durchzuführen, obwohl es möglicherweise besser ist, dies in der Standardisierung zu tun (siehe https://github.com/mnot/thor/blob/master/thor/events.py und https://github.com). .com /mnot/thor/blob/master/doc/events.md )
Referenzen
PEP 380 Von TBD: Greg Ewings Tutorial beschreibt die Semantik von yield.
PEP 3148 beschreibt concurrent.futures.Future.
PEP 3153 wird zwar abgelehnt, beschreibt aber gut die Notwendigkeit, Transporte und Protokolle zu trennen.
Tulip Repo: http://code.google.com/p/tulip/
Ein guter Blogeintrag von Nick Coghlan mit etwas Kontext, über asynchrone E/A als unterschiedliche Verarbeitung, gevent und Ideen zur Verwendung zukünftiger Konzepte wie wihle, for und with: http://python-notes.boredomandlaziness.org/en/latest/pep_ideas/async_programming.html
TBD: Referenzen zu Twisted, Tornado, ZeroMQ, pyftpdlib, libevent, libev, pyev, libuv, wattle usw.
Danksagungen
Zusätzlich zu PEP 3153, beeinflusst von Zu den Einflüssen gehören PEP 380 und Greg Ewings Tutorial yielding from, Twisted, Tornado, ZeroMQ, pyftpdlib, tulip (der Versuch des Autors, diese alle zu kombinieren), wattle (Steve Dowers Gegenvorschlag), September 2012. Bis Dezember gab es viele Diskussionen über Python-Ideen, eine Skype-Sitzung mit Steve Dower und Dino Viehland, ein E-Mail-Austausch mit Ben Darnell, ein Zuhörer mit Niels Provos (ursprünglicher Autor von libevent), zwei Shows und mehrere Twisted-Entwickler. Persönliche Treffen mit Entwicklern, darunter Glyph, Brian Warner, David Reid und Duncan McGreggor. In ähnlicher Weise hatte der Autor in der frühen Phase auch einen wichtigen Einfluss auf die asynchrone Unterstützung der NDB-Bibliothek von Google App Engine.