Diese Frage ist durch meine andere Frage motiviert: Wie kann man in cdef warten?
Es gibt Unmengen von Artikeln und Blog-Posts im Web asyncio
, aber sie sind alle sehr oberflächlich. Ich konnte keine Informationen darüber finden, wie die asyncio
Implementierung tatsächlich erfolgt und was die E / A asynchron macht. Ich habe versucht, den Quellcode zu lesen, aber es sind Tausende von Zeilen nicht des C-Codes der höchsten Klasse, von denen sich viele mit Hilfsobjekten befassen, aber vor allem ist es schwierig, eine Verbindung zwischen der Python-Syntax und dem zu übersetzenden C-Code herzustellen in.
Asycnios eigene Dokumentation ist noch weniger hilfreich. Es gibt dort keine Informationen darüber, wie es funktioniert, nur einige Richtlinien zur Verwendung, die manchmal auch irreführend / sehr schlecht geschrieben sind.
Ich bin mit der Implementierung von Coroutinen durch Go vertraut und hatte gehofft, dass Python dasselbe tut. Wenn dies der Fall wäre, hätte der Code, den ich in dem oben verlinkten Beitrag gefunden habe, funktioniert. Da dies nicht der Fall war, versuche ich jetzt herauszufinden, warum. Meine bisher beste Vermutung lautet wie folgt: Bitte korrigieren Sie mich, wo ich falsch liege:
- Prozedurdefinitionen des Formulars
async def foo(): ...
werden tatsächlich als Methoden einer erbenden Klasse interpretiertcoroutine
. - Möglicherweise wird
async def
es tatsächlich durchawait
Anweisungen in mehrere Methoden aufgeteilt , wobei das Objekt, für das diese Methoden aufgerufen werden, den Fortschritt verfolgen kann, den es bisher durch die Ausführung erzielt hat. - Wenn das oben Gesagte zutrifft, läuft die Ausführung einer Coroutine im Wesentlichen darauf hinaus, Methoden eines Coroutine-Objekts durch einen globalen Manager (Schleife?) Aufzurufen.
- Der globale Manager weiß irgendwie (wie?), Wann E / A-Operationen von Python-Code (nur?) Ausgeführt werden, und kann eine der ausstehenden Coroutine-Methoden auswählen, die ausgeführt werden sollen, nachdem die aktuelle ausführende Methode die Kontrolle aufgegeben hat (auf die
await
Anweisung klicken) ).
Mit anderen Worten, hier ist mein Versuch, eine asyncio
Syntax in etwas Verständlicheres zu "entschärfen" :
async def coro(name):
print('before', name)
await asyncio.sleep()
print('after', name)
asyncio.gather(coro('first'), coro('second'))
# translated from async def coro(name)
class Coro(coroutine):
def before(self, name):
print('before', name)
def after(self, name):
print('after', name)
def __init__(self, name):
self.name = name
self.parts = self.before, self.after
self.pos = 0
def __call__():
self.parts[self.pos](self.name)
self.pos += 1
def done(self):
return self.pos == len(self.parts)
# translated from asyncio.gather()
class AsyncIOManager:
def gather(*coros):
while not every(c.done() for c in coros):
coro = random.choice(coros)
coro()
Sollte sich meine Vermutung als richtig erweisen, dann habe ich ein Problem. Wie geschieht E / A in diesem Szenario? In einem separaten Thread? Ist der gesamte Dolmetscher gesperrt und erfolgt die E / A außerhalb des Dolmetschers? Was genau ist mit E / A gemeint? Wenn meine Python-Prozedur die C-Prozedur aufgerufen open()
hat und ihrerseits einen Interrupt an den Kernel gesendet hat und die Kontrolle an ihn abgegeben hat, woher weiß der Python-Interpreter davon und kann weiterhin anderen Code ausführen, während der Kernel-Code die eigentliche E / A ausführt und bis es weckt die Python-Prozedur, die den Interrupt ursprünglich gesendet hat? Wie kann sich der Python-Interpreter im Prinzip dessen bewusst sein?
quelle
BaseEventLoop
implementiert ist: github.com/python/cpython/blob/…_run_once
, was eigentlich die einzige nützliche Funktion in diesem gesamten Modul ist, "privat" gemacht? Die Implementierung ist schrecklich, aber das ist weniger ein Problem. Warum ist die einzige Funktion, die Sie jemals in der Ereignisschleife aufrufen möchten, als "Rufen Sie mich nicht an" markiert?_run_once
?_run_once
?asyncio
ist komplex und hat seine Fehler, aber bitte halten Sie die Diskussion zivil. Machen Sie den Entwicklern hinter Code, den Sie selbst nicht verstehen, nicht schlecht.Antworten:
Wie funktioniert Asyncio?
Bevor wir diese Frage beantworten, müssen wir einige Grundbegriffe verstehen. Überspringen Sie diese, wenn Sie bereits einen kennen.
Generatoren
Generatoren sind Objekte, mit denen wir die Ausführung einer Python-Funktion unterbrechen können. Vom Benutzer kuratierte Generatoren werden mithilfe des Schlüsselworts implementiert
yield
. Indemyield
wir eine normale Funktion erstellen, die das Schlüsselwort enthält, verwandeln wir diese Funktion in einen Generator:Wie Sie sehen können,
next()
lädt der Interpreter beim Aufrufen des Generators den Testrahmen und gibt denyield
ed-Wert zurück. Wenn Sienext()
erneut aufrufen , wird der Frame erneut in den Interpreter-Stack geladen, und fahrenyield
Sie mit einem anderen Wert fort.Beim dritten
next()
Aufruf war unser Generator fertig undStopIteration
wurde geworfen.Kommunikation mit einem Generator
Ein weniger bekanntes Merkmal von Generatoren ist die Tatsache, dass Sie mit ihnen auf zwei Arten kommunizieren können:
send()
undthrow()
.Beim Aufruf
gen.send()
wird der Wert als Rückgabewert vomyield
Schlüsselwort übergeben.gen.throw()
Auf der anderen Seite können Ausnahmen in Generatoren ausgelöst werden, wobei die an derselben Stelle ausgelöste Ausnahmeyield
aufgerufen wurde.Rückgabe von Werten von Generatoren
Wenn Sie einen Wert von einem Generator zurückgeben, wird der Wert in die
StopIteration
Ausnahme eingefügt. Wir können den Wert später aus der Ausnahme wiederherstellen und nach Bedarf verwenden.Siehe, ein neues Schlüsselwort:
yield from
Python 3.4 wurde mit einem neuen Schlüsselwort versehen :
yield from
. Was das Schlüsselwort uns erlaubt , zu tun ist auf jedem passierennext()
,send()
undthrow()
in einen inneren am weitesten verschachtelten Generator. Wenn der innere Generator einen Wert zurückgibt, ist dies auch der Rückgabewert vonyield from
:Ich habe einen Artikel geschrieben , um dieses Thema weiter zu erläutern.
Alles zusammenfügen
Mit der Einführung des neuen Schlüsselworts
yield from
in Python 3.4 konnten wir nun Generatoren in Generatoren erstellen, die genau wie ein Tunnel die Daten vom innersten zum äußersten Generator hin und her übertragen. Dies hat eine neue Bedeutung für Generatoren hervorgebracht - Coroutinen .Coroutinen sind Funktionen, die während der Ausführung gestoppt und fortgesetzt werden können. In Python werden sie mit dem
async def
Schlüsselwort definiert . Ähnlich wie Generatoren, verwenden sie auch ihre eigene Formyield from
davon istawait
. Vorasync
undawait
in Python 3.5 eingeführt, haben wir Coroutinen genauso erstellt, wie Generatoren erstellt wurden (mityield from
stattawait
).Wie jeder Iterator oder Generator, der die
__iter__()
Methode implementiert , werden Coroutinen implementiert,__await__()
die es ihnen ermöglichen, bei jedemawait coro
Aufruf fortzufahren .In den Python-Dokumenten befindet sich ein schönes Sequenzdiagramm , das Sie überprüfen sollten.
In asyncio haben wir neben Coroutine-Funktionen zwei wichtige Objekte: Aufgaben und Zukunft .
Futures
Futures sind Objekte, bei denen die
__await__()
Methode implementiert ist und deren Aufgabe es ist, einen bestimmten Status und ein bestimmtes Ergebnis zu halten. Der Zustand kann einer der folgenden sein:fut.cancel()
fut.set_result()
oder durch eine Ausnahmesatz mit beendetfut.set_exception()
Das Ergebnis kann, wie Sie vermutet haben, entweder ein Python-Objekt sein, das zurückgegeben wird, oder eine Ausnahme, die möglicherweise ausgelöst wird.
Ein weiteres wichtiges Merkmal von
future
Objekten ist, dass sie eine aufgerufene Methode enthaltenadd_done_callback()
. Mit dieser Methode können Funktionen aufgerufen werden, sobald die Aufgabe erledigt ist - unabhängig davon, ob eine Ausnahme ausgelöst oder beendet wurde.Aufgaben
Aufgabenobjekte sind spezielle Zukünfte, die sich um Coroutinen wickeln und mit den innersten und äußersten Coroutinen kommunizieren. Jedes Mal, wenn eine Coroutine eine
await
Zukunft hat, wird die Zukunft vollständig an die Aufgabe zurückgegeben (genau wie inyield from
), und die Aufgabe erhält sie.Als nächstes bindet sich die Aufgabe an die Zukunft. Dies geschieht durch einen Aufruf
add_done_callback()
an die Zukunft. Von nun an wird der Rückruf der Aufgabe aufgerufen, wenn die Zukunft jemals durch Abbrechen, Übergeben einer Ausnahme oder Übergeben eines Python-Objekts erreicht wird.Asyncio
Die letzte brennende Frage, die wir beantworten müssen, lautet: Wie wird das E / A implementiert?
Tief in Asyncio haben wir eine Ereignisschleife. Eine Ereignisschleife von Aufgaben. Die Aufgabe der Ereignisschleife besteht darin, Aufgaben jedes Mal aufzurufen, wenn sie bereit sind, und all diese Anstrengungen in einer einzigen Arbeitsmaschine zu koordinieren.
Der E / A-Teil der Ereignisschleife basiert auf einer einzelnen entscheidenden Funktion, die aufgerufen wird
select
. Select ist eine Blockierungsfunktion, die vom darunter liegenden Betriebssystem implementiert wird und das Warten auf eingehende oder ausgehende Daten auf Sockets ermöglicht. Wenn Daten empfangen werden, werden sie aktiviert und geben die Sockets zurück, die Daten empfangen haben, oder die Sockets, die zum Schreiben bereit sind.Wenn Sie versuchen, Daten über einen Socket über Asyncio zu empfangen oder zu senden, geschieht im Folgenden tatsächlich, dass der Socket zuerst überprüft wird, ob Daten vorhanden sind, die sofort gelesen oder gesendet werden können. Wenn der
.send()
Puffer voll ist oder der.recv()
Puffer leer ist, wird der Socket für dieselect
Funktion registriert (indem er einfachrlist
fürrecv
undwlist
für zu einer der Listen hinzugefügt wirdsend
), und die entsprechende Funktion ist einawait
neu erstelltesfuture
Objekt, das an diesen Socket gebunden ist.Wenn alle verfügbaren Aufgaben auf Futures warten, ruft die Ereignisschleife auf
select
und wartet. Wenn auf einem der Sockets eingehende Daten vorhanden sind oder dersend
Puffer leer ist, sucht Asyncio nach dem zukünftigen Objekt, das an diesen Socket gebunden ist, und setzt es auf Fertig.Jetzt passiert die ganze Magie. Die Zukunft ist erledigt, die Aufgabe, die sich zuvor mit hinzugefügt hat
add_done_callback()
, wird wieder lebendig und ruft.send()
die Coroutine auf, die die innerste Coroutine (aufgrund derawait
Kette) wieder aufnimmt, und Sie lesen die neu empfangenen Daten aus einem nahe gelegenen Puffer wurde verschüttet.Wieder eine Methodenkette bei
recv()
:select.select
wartet.future.set_result()
wird genannt.add_done_callback()
wird jetzt aktiviert..send()
die Coroutine auf, die bis in die innerste Coroutine reicht und diese aufweckt.Zusammenfassend lässt sich sagen, dass Asyncio Generatorfunktionen verwendet, mit denen Funktionen angehalten und fortgesetzt werden können. Es verwendet
yield from
Funktionen, mit denen Daten vom innersten zum äußersten Generator hin und her übertragen werden können. Es nutzt alle diejenigen , um Halt Funktion Ausführung , während es für IO , um eine vollständige warten (durch das Betriebssystem mitselect
Funktion).Und das Beste von allem? Während eine Funktion angehalten wird, kann eine andere ausgeführt werden und sich mit dem empfindlichen Stoff verschachteln, der asynchron ist.
quelle
yield from
funktioniert. Ich habe jedoch oben bemerkt, dass es übersprungen werden kann, falls der Leser bereits davon weiß :-) Glaubst du noch etwas, das ich hinzufügen sollte?select
kann sich auch qualifizieren, da nicht blockierende E / A-Systemaufrufe unter Betriebssystem funktionieren. Die eigentlichenasyncio
Konstrukte und die Ereignisschleife sind nur Code auf App-Ebene, der aus diesen Dingen erstellt wurde.Sprechen
async/await
undasyncio
ist nicht dasselbe. Das erste ist ein grundlegendes Konstrukt auf niedriger Ebene (Coroutinen), während das letztere eine Bibliothek ist, die diese Konstrukte verwendet. Umgekehrt gibt es keine einzige endgültige Antwort.Das Folgende ist eine allgemeine Beschreibung, wie
async/await
undasyncio
-ähnlichen Bibliotheken arbeiten. Das heißt, es gibt vielleicht noch andere Tricks (es gibt ...), aber sie spielen keine Rolle, es sei denn, Sie bauen sie selbst. Der Unterschied sollte vernachlässigbar sein, es sei denn, Sie wissen bereits genug, um eine solche Frage nicht stellen zu müssen.1. Coroutinen versus Subroutinen in einer Nussschale
Genau wie Unterprogramme (Funktionen, Prozeduren, ...) sind Coroutinen (Generatoren, ...) eine Abstraktion von Aufrufstapel und Anweisungszeiger: Es gibt einen Stapel ausführender Codeteile, und jedes befindet sich an einem bestimmten Befehl.
Die Unterscheidung zwischen
def
versusasync def
dient lediglich der Klarheit. Der tatsächliche Unterschied istreturn
versusyield
. Darausawait
oderyield from
nehmen Sie die Differenz von einzelnen Anrufen ganzen Stapel.1.1. Unterprogramme
Eine Unterroutine repräsentiert eine neue Stapelebene zum Speichern lokaler Variablen und ein einzelnes Durchlaufen ihrer Anweisungen, um ein Ende zu erreichen. Stellen Sie sich ein Unterprogramm wie das folgende vor:
Wenn Sie es ausführen, bedeutet das
bar
und zuqux
return
, drücken Sie den Wert auf den anrufenden StapelInsbesondere bedeutet 4., dass eine Unterroutine immer im selben Zustand beginnt. Alles, was nur für die Funktion selbst gilt, geht nach Abschluss verloren. Eine Funktion kann nicht wieder aufgenommen werden, auch wenn danach Anweisungen vorliegen
return
.1.2. Coroutinen als persistente Unterprogramme
Eine Coroutine ist wie eine Subroutine, kann jedoch beendet werden, ohne ihren Zustand zu zerstören. Stellen Sie sich eine Coroutine wie diese vor:
Wenn Sie es ausführen, bedeutet das
bar
und zuqux
yield
schieben seinen Wert an den anrufenden Stapel , aber Speichern des Stapels und Befehlszeigeryield
den Stapel- und Anweisungszeiger wieder her und drücken Sie die Argumente anqux
return
, drücken Sie den Wert auf den anrufenden StapelBeachten Sie die Hinzufügung von 2.1 und 2.2 - eine Coroutine kann an vordefinierten Punkten ausgesetzt und wieder aufgenommen werden. Dies ähnelt dem Anhalten einer Unterroutine beim Aufrufen einer anderen Unterroutine. Der Unterschied besteht darin, dass die aktive Coroutine nicht streng an ihren aufrufenden Stapel gebunden ist. Stattdessen ist eine suspendierte Coroutine Teil eines separaten, isolierten Stapels.
Dies bedeutet, dass suspendierte Coroutinen frei gelagert oder zwischen Stapeln bewegt werden können. Jeder Aufrufstapel, der Zugriff auf eine Coroutine hat, kann diese fortsetzen.
1.3. Durchlaufen des Aufrufstapels
Bisher geht unsere Coroutine nur mit den Call-Stack runter
yield
. Eine Unterroutine kann den Aufrufstapel mit und nach unten und oben gehen . Der Vollständigkeit halber benötigen Coroutinen auch einen Mechanismus, um den Aufrufstapel zu erhöhen. Stellen Sie sich eine Coroutine wie diese vor:return
()
Wenn Sie es ausführen, bedeutet dies, dass der Stapel und der Anweisungszeiger weiterhin wie eine Unterroutine zugewiesen werden. Wenn es angehalten wird, ist das immer noch wie das Speichern einer Unterroutine.
Allerdings
yield from
tut beides . Es verschiebt Stapel und Befehlszeigerwrap
und läuftcofoo
. Beachten Sie, dasswrap
bis zumcofoo
vollständigen Abschluss ausgesetzt bleibt . Immer wenncofoo
angehalten oder etwas gesendet wird,cofoo
ist es direkt mit dem aufrufenden Stack verbunden.1.4. Coroutinen ganz nach unten
yield from
Ermöglicht, wie festgelegt, das Verbinden von zwei Bereichen über einen anderen Zwischenbereich. Bei rekursiver Anwendung bedeutet dies, dass die Oberseite des Stapels mit der Unterseite des Stapels verbunden werden kann.Beachten Sie das
root
undcoro_b
wissen nicht voneinander. Dies macht Coroutinen viel sauberer als Rückrufe: Coroutinen bauen immer noch auf einer 1: 1-Beziehung auf, wie Subroutinen. Coroutinen setzen ihren gesamten vorhandenen Ausführungsstapel bis zu einem regulären Aufrufpunkt aus und setzen ihn fort.Insbesondere
root
könnte eine beliebige Anzahl von Coroutinen wieder aufgenommen werden. Es kann jedoch niemals mehr als eine gleichzeitig wieder aufnehmen. Coroutinen derselben Wurzel sind gleichzeitig, aber nicht parallel!1.5. Pythons
async
undawait
Die Erklärung hat bisher ausdrücklich die verwendet
yield
undyield from
Vokabular von Generatoren - die zugrunde liegende Funktionalität ist das gleiche. Die neue Python3.5 Syntaxasync
undawait
besteht in erster Linie aus Gründen der Übersichtlichkeit.Die Anweisungen
async for
undasync with
werden benötigt, da Sie dieyield from/await
Kette mit den Anweisungen barefor
undwith
Anweisungen unterbrechen würden.2. Anatomie einer einfachen Ereignisschleife
An sich hat eine Coroutine kein Konzept, einer anderen Coroutine die Kontrolle zu geben . Es kann nur dem Aufrufer am unteren Rand eines Coroutine-Stapels die Kontrolle geben. Dieser Anrufer kann dann zu einer anderen Coroutine wechseln und diese ausführen.
Dieser Wurzelknoten mehrerer Coroutinen ist üblicherweise eine Ereignisschleife : Bei Suspendierung liefert eine Coroutine ein Ereignis, bei dem sie fortgesetzt werden soll. Die Ereignisschleife kann wiederum effizient auf das Auftreten dieser Ereignisse warten. Auf diese Weise kann entschieden werden, welche Coroutine als Nächstes ausgeführt werden soll oder wie gewartet werden soll, bevor die Wiederaufnahme fortgesetzt wird.
Ein solches Design impliziert, dass es eine Reihe vordefinierter Ereignisse gibt, die die Schleife versteht. Mehrere Coroutinen
await
einander, bis schließlich ein Ereignisawait
ed ist. Dieses Ereignis kann durch Steuerung direkt mit der Ereignisschleife kommunizierenyield
.Der Schlüssel ist, dass die Coroutine-Suspendierung die direkte Kommunikation zwischen Ereignisschleife und Ereignissen ermöglicht. Der Zwischen-Coroutine-Stapel erfordert keine Kenntnisse darüber, welche Schleife ihn ausführt oder wie Ereignisse funktionieren.
2.1.1. Ereignisse in der Zeit
Das am einfachsten zu behandelnde Ereignis ist das Erreichen eines Zeitpunkts. Dies ist ebenfalls ein grundlegender Block von Thread-Code: Ein Thread wiederholt
sleep
s, bis eine Bedingung erfüllt ist. Eine reguläresleep
Blockierung der Ausführung von selbst - wir möchten, dass andere Coroutinen nicht blockiert werden. Stattdessen möchten wir der Ereignisschleife mitteilen, wann sie den aktuellen Coroutine-Stapel wieder aufnehmen soll.2.1.2. Ein Ereignis definieren
Ein Ereignis ist einfach ein Wert, den wir identifizieren können - sei es über eine Aufzählung, einen Typ oder eine andere Identität. Wir können dies mit einer einfachen Klasse definieren, die unsere Zielzeit speichert. Zusätzlich zum Speichern der Ereignisinformationen können wir
await
einer Klasse direkt erlauben .Diese Klasse speichert nur das Ereignis - sie sagt nicht aus, wie sie tatsächlich behandelt werden soll.
Die einzige Besonderheit ist
__await__
- es ist das, wonach dasawait
Schlüsselwort sucht. Praktisch ist es ein Iterator, aber nicht für die reguläre Iterationsmaschinerie verfügbar.2.2.1. Warten auf ein Ereignis
Wie reagieren Coroutinen nach einem Ereignis darauf? Wir sollten in der Lage sein, das Äquivalent von
sleep
durchawait
unsere Veranstaltung auszudrücken . Um besser zu sehen, was los ist, warten wir zweimal die halbe Zeit:Wir können diese Coroutine direkt instanziieren und ausführen. Ähnlich wie bei einem Generator wird
coroutine.send
die Coroutine mit verwendet, bis einyield
Ergebnis erzielt wird .Dies gibt uns zwei
AsyncSleep
Ereignisse und dann ein,StopIteration
wenn die Coroutine fertig ist. Beachten Sie, dass die einzige Verzögerung vontime.sleep
in der Schleife ist! JederAsyncSleep
speichert nur einen Versatz von der aktuellen Zeit.2.2.2. Ereignis + Schlaf
Zu diesem Zeitpunkt stehen uns zwei separate Mechanismen zur Verfügung:
AsyncSleep
Ereignisse, die innerhalb einer Coroutine ausgelöst werden könnentime.sleep
das kann warten, ohne die Coroutinen zu beeinträchtigenBemerkenswerterweise sind diese beiden orthogonal: Keiner beeinflusst oder löst den anderen aus. Infolgedessen können wir unsere eigene Strategie entwickeln
sleep
, um die Verzögerung eines zu bewältigenAsyncSleep
.2.3. Eine naive Ereignisschleife
Wenn wir mehrere Coroutinen haben, kann jeder uns sagen, wann er geweckt werden möchte. Wir können dann warten, bis der erste von ihnen wieder aufgenommen werden möchte, dann auf den nachfolgenden und so weiter. Insbesondere kümmern wir uns an jedem Punkt nur darum, welcher der nächste ist .
Dies ermöglicht eine einfache Planung:
Eine triviale Implementierung erfordert keine fortgeschrittenen Konzepte. A
list
ermöglicht das Sortieren von Coroutinen nach Datum. Warten ist eine regelmäßigetime.sleep
. Das Ausführen von Coroutinen funktioniert wie zuvor mitcoroutine.send
.Dies hat natürlich viel Raum für Verbesserungen. Wir können einen Heap für die Warteschlange oder eine Versandtabelle für Ereignisse verwenden. Wir könnten auch Rückgabewerte von der
StopIteration
abrufen und sie der Coroutine zuweisen. Das Grundprinzip bleibt jedoch dasselbe.2.4. Genossenschaftliches Warten
Das
AsyncSleep
Ereignis und dierun
Ereignisschleife sind eine voll funktionsfähige Implementierung von zeitgesteuerten Ereignissen.Dies schaltet kooperativ zwischen jeder der fünf Coroutinen um und unterbricht jede für 0,1 Sekunden. Obwohl die Ereignisschleife synchron ist, führt sie die Arbeit in 0,5 Sekunden statt in 2,5 Sekunden aus. Jede Coroutine hält den Zustand und handelt unabhängig.
3. E / A-Ereignisschleife
Eine Ereignisschleife, die unterstützt,
sleep
eignet sich zum Abrufen . Das Warten auf E / A in einem Dateihandle kann jedoch effizienter durchgeführt werden: Das Betriebssystem implementiert E / A und weiß somit, welche Handles bereit sind. Im Idealfall sollte eine Ereignisschleife ein explizites "Bereit für E / A" -Ereignis unterstützen.3.1. Der
select
AnrufPython verfügt bereits über eine Schnittstelle zum Abfragen des Betriebssystems nach Lese-E / A-Handles. Beim Aufruf mit Handles zum Lesen oder Schreiben werden die Handles zurückgegeben , die zum Lesen oder Schreiben bereit sind :
Zum Beispiel können wir
open
eine Datei zum Schreiben erstellen und warten, bis sie fertig ist:Sobald select zurückkehrt,
writeable
enthält unsere geöffnete Datei.3.2. Grundlegendes E / A-Ereignis
Ähnlich wie bei der
AsyncSleep
Anforderung müssen wir ein Ereignis für E / A definieren. Mit der zugrunde liegendenselect
Logik muss sich das Ereignis auf ein lesbares Objekt beziehen - beispielsweise eineopen
Datei. Außerdem speichern wir, wie viele Daten gelesen werden sollen.Wie bei
AsyncSleep
speichern wir meist nur die Daten, die für den zugrunde liegenden Systemaufruf erforderlich sind. Dieses Mal kann__await__
es mehrmals fortgesetzt werden - bis unser Wunschamount
gelesen wurde. Darüber hinaus erhalten wirreturn
das E / A-Ergebnis, anstatt nur fortzufahren.3.3. Erweitern einer Ereignisschleife mit Lese-E / A.
Die Basis für unsere Ereignisschleife ist immer noch die
run
zuvor definierte. Zuerst müssen wir die Leseanforderungen verfolgen. Dies ist kein sortierter Zeitplan mehr, wir ordnen nur Leseanforderungen Coroutinen zu.Da
select.select
ein Timeout-Parameter benötigt wird, können wir ihn anstelle von verwendentime.sleep
.Dies gibt uns alle lesbaren Dateien - falls vorhanden, führen wir die entsprechende Coroutine aus. Wenn es keine gibt, haben wir lange genug darauf gewartet, dass unsere aktuelle Coroutine ausgeführt wird.
Schließlich müssen wir tatsächlich auf Leseanfragen warten.
3.4. Etwas zusammensetzen
Das Obige war eine kleine Vereinfachung. Wir müssen etwas wechseln, um schlafende Coroutinen nicht zu verhungern, wenn wir immer lesen können. Wir müssen damit umgehen, nichts zu lesen oder zu warten. Das Endergebnis passt jedoch immer noch in 30 LOC.
3.5. Genossenschaftliche E / A.
Die
AsyncSleep
,AsyncRead
undrun
Implementierungen sind jetzt voll funktionsfähig zu schlafen und / oder zu lesen. Wie fürsleepy
können wir einen Helfer definieren, um das Lesen zu testen:Wenn wir dies ausführen, können wir sehen, dass unsere E / A mit der Warteaufgabe verschachtelt ist:
4. Nicht blockierende E / A.
Während E / A für Dateien das Konzept vermittelt, ist es für eine Bibliothek wie diese nicht wirklich geeignet
asyncio
: Derselect
Aufruf wird immer für Dateien und beides zurückgegebenopen
undread
kann auf unbestimmte Zeit blockiert werden . Dies blockiert alle Coroutinen einer Ereignisschleife - was schlecht ist. Bibliotheken wieaiofiles
Threads und Synchronisation verwenden, um nicht blockierende E / A und Ereignisse in der Datei zu fälschen.Sockets ermöglichen jedoch nicht blockierende E / A - und ihre inhärente Latenz macht sie viel kritischer. Bei Verwendung in einer Ereignisschleife kann das Warten auf Daten und das erneute Versuchen abgeschlossen werden, ohne dass etwas blockiert wird.
4.1. Nicht blockierendes E / A-Ereignis
Ähnlich wie bei uns
AsyncRead
können wir ein Suspend-and-Read-Ereignis für Sockets definieren. Anstatt eine Datei zu nehmen, nehmen wir einen Socket - der nicht blockierend sein darf. Auch unsere__await__
Verwendungensocket.recv
anstelle vonfile.read
.Im Gegensatz zu
AsyncRead
,__await__
blockiert nicht-führt wirklich I / O. Wenn Daten verfügbar sind, werden sie immer gelesen. Wenn keine Daten verfügbar sind, werden diese immer angehalten. Das heißt, die Ereignisschleife wird nur blockiert, während wir nützliche Arbeit leisten.4.2. Entsperren der Ereignisschleife
An der Ereignisschleife ändert sich nicht viel. Das Ereignis, auf das gewartet werden soll, ist immer noch dasselbe wie für Dateien - ein Dateideskriptor, der als bereit markiert ist
select
.An diesem Punkt sollte es offensichtlich sein, dass
AsyncRead
undAsyncRecv
sind die gleiche Art von Ereignis. Wir könnten sie leicht zu einem Ereignis mit einer austauschbaren E / A-Komponente umgestalten . Tatsächlich trennen die Ereignisschleife, Coroutinen und Ereignisse einen Scheduler, einen beliebigen Zwischencode und die tatsächliche E / A sauber voneinander .4.3. Die hässliche Seite der nicht blockierenden E / A.
Im Prinzip sollten Sie an dieser Stelle die Logik von
read
asrecv
for wiederholenAsyncRecv
. Dies ist jetzt jedoch viel hässlicher - Sie müssen mit frühen Rückgaben umgehen, wenn Funktionen im Kernel blockieren, aber Ihnen die Kontrolle geben. Zum Beispiel ist das Öffnen einer Verbindung viel länger als das Öffnen einer Datei:Kurz gesagt, es bleiben ein paar Dutzend Zeilen für die Ausnahmebehandlung. Die Ereignisse und die Ereignisschleife funktionieren bereits zu diesem Zeitpunkt.
Nachtrag
Beispielcode bei github
quelle
yield self
in AsyncSleep gibt mir einenTask got back yield
Fehler. Warum ist das so? Ich sehe, dass der Code in asyncio.Futures das verwendet. Die Verwendung einer bloßen Ausbeute funktioniert gut.Ihre
coro
Desugaring ist konzeptionell korrekt, aber etwas unvollständig.await
wird nicht unbedingt angehalten, sondern nur, wenn ein blockierender Anruf auftritt. Woher weiß es, dass ein Anruf blockiert wird? Dies wird durch den erwarteten Code entschieden. Zum Beispiel könnte eine erwartete Implementierung des Socket-Lesens entschärft werden für:In echtem Asyncio der entsprechende Code den Status von a,
Future
anstatt magische Werte zurückzugeben, aber das Konzept ist dasselbe. Bei entsprechender Anpassung an ein generatorähnliches Objekt kann der obige Code bearbeitet werdenawait
.Auf der Anruferseite, wenn Ihre Coroutine enthält:
Es entgiftet in etwas in der Nähe von:
Leute, die mit Generatoren vertraut sind, neigen dazu, das Obige in Bezug auf zu beschreiben
yield from
die Federung automatisch erfolgt.Die Aufhängungskette wird bis zur Ereignisschleife fortgesetzt, die feststellt, dass die Coroutine angehalten ist, sie aus dem ausführbaren Satz entfernt und gegebenenfalls ausführbare Coroutinen ausführt. Wenn keine Coroutinen ausgeführt werden können, wartet die Schleife,
select()
bis einer der Dateideskriptoren, an denen eine Coroutine interessiert ist, für die E / A bereit ist. (Die Ereignisschleife verwaltet eine Zuordnung von Dateideskriptor zu Coroutine.)Im obigen Beispiel wird die lesbare
select()
Ereignisschleifesock
erneut hinzugefügt , sobald sie lesbar istcoro
zum ausführbaren Satz , sodass sie ab dem Punkt der Unterbrechung fortgesetzt wird.Mit anderen Worten:
Standardmäßig geschieht alles im selben Thread.
Die Ereignisschleife ist dafür verantwortlich, die Coroutinen zu planen und aufzuwecken, wenn alles, worauf sie gewartet haben (normalerweise ein normalerweise blockierender E / A-Aufruf oder eine Zeitüberschreitung), bereit ist.
Für einen Einblick in Coroutine-treibende Event-Loops empfehle ich diesen Vortrag von Dave Beazley, in dem er vor einem Live-Publikum das Codieren eines Event-Loops von Grund auf demonstriert.
quelle
async.wait_for()
es nicht das tut, was es soll ... Warum ist es so ein großes Problem, der Ereignisschleife einen Rückruf hinzuzufügen und es zu sagen um so viele Rückrufe zu verarbeiten, wie es benötigt wird, einschließlich des gerade hinzugefügten? Meine Frustration darüberasyncio
ist teilweise auf die Tatsache zurückzuführen, dass das zugrunde liegende Konzept sehr einfach ist und Emacs Lisp beispielsweise seit Ewigkeiten implementiert wurde, ohne Schlagworte zu verwenden ... (dhcreate-async-process
undaccept-process-output
- und das ist alles, was benötigt wird ... (Fortsetzung)wait_for
tut, was es soll (es ist eine Coroutine, auf die Sie warten sollten), sondern dass Ihre Erwartungen nicht mit dem übereinstimmen, wofür das System entwickelt und implementiert wurde. Ich denke, Ihr Problem könnte mit asyncio abgeglichen werden, wenn die Ereignisschleife in einem separaten Thread ausgeführt würde, aber ich kenne die Details Ihres Anwendungsfalls nicht und ehrlich gesagt macht es Ihre Einstellung nicht viel Spaß, Ihnen zu helfen.My frustration with asyncio is in part due to the fact that the underlying concept is very simple, and, for example, Emacs Lisp had implementation for ages, without using buzzwords...
- Nichts hindert Sie daran, dieses einfache Konzept ohne Schlagworte für Python zu implementieren :) Warum verwenden Sie dieses hässliche Asyncio überhaupt? Implementieren Sie Ihre eigenen von Grund auf neu. Sie können beispielsweise damit beginnen, eine eigeneasync.wait_for()
Funktion zu erstellen , die genau das tut, was sie soll.asyncio
. Aber im Prinzip ist das nicht meine Entscheidung. Ich bin gezwungen, über en.wikipedia.org/wiki/Ultimatum_game die Müllsprache zu verwenden .Alles läuft auf die beiden Hauptherausforderungen hinaus, mit denen sich Asyncio befasst:
Die Antwort auf den ersten Punkt gibt es schon lange und wird als Auswahlschleife bezeichnet . In Python ist es im Selektormodul implementiert .
Die zweite Frage bezieht sich auf das Konzept der Coroutine , dh Funktionen, die ihre Ausführung stoppen und später wiederhergestellt werden können. In Python werden Coroutinen mithilfe von Generatoren und dem Ertrag aus der Anweisung implementiert . Das ist es, was sich hinter dem versteckt asynchronen / wartenden Syntax verbirgt .
Weitere Ressourcen in dieser Antwort .
BEARBEITEN: Adressierung Ihres Kommentars zu Goroutinen:
Das nächste Äquivalent zu einer Goroutine in Asyncio ist eigentlich keine Coroutine, sondern eine Aufgabe (siehe den Unterschied in der Dokumentation ). In Python weiß eine Coroutine (oder ein Generator) nichts über die Konzepte der Ereignisschleife oder der E / A. Es ist einfach eine Funktion, die die Ausführung unter
yield
Beibehaltung des aktuellen Status stoppen kann , sodass sie später wiederhergestellt werden kann. Dieyield from
Syntax ermöglicht eine transparente Verkettung.Innerhalb einer Asyncio-Aufgabe ergibt die Coroutine ganz unten in der Kette immer eine Zukunft . Diese Zukunft sprudelt dann in die Ereignisschleife und wird in die innere Maschinerie integriert. Wenn die Zukunft durch einen anderen inneren Rückruf festgelegt wird, kann die Ereignisschleife die Aufgabe wiederherstellen, indem die Zukunft zurück in die Coroutine-Kette gesendet wird.
BEARBEITEN: Beantworten einiger Fragen in Ihrem Beitrag:
Nein, in einem Thread passiert nichts. E / A wird immer von der Ereignisschleife verwaltet, hauptsächlich über Dateideskriptoren. Die Registrierung dieser Dateideskriptoren wird jedoch normalerweise von hochrangigen Coroutinen ausgeblendet, sodass die Drecksarbeit für Sie erledigt wird.
Eine E / A ist ein blockierender Anruf. In asyncio sollten alle E / A-Vorgänge die Ereignisschleife durchlaufen, da die Ereignisschleife, wie Sie sagten, nicht erkennen kann, dass ein blockierender Aufruf in einem synchronen Code ausgeführt wird. Das heißt, Sie sollten keine Synchronisation
open
im Kontext einer Coroutine verwenden. Verwenden Sie stattdessen eine dedizierte Bibliothek wie aiofiles, die eine asynchrone Version von bereitstelltopen
.quelle
yield from
sagt eigentlich nichts aus.yield from
ist nur ein Syntaxkonstrukt, es ist kein grundlegender Baustein, den Computer ausführen können. Ebenso für Auswahlschleife. Ja, Coroutinen in Go verwenden auch eine Select-Schleife, aber was ich versucht habe, würde in Go funktionieren, aber nicht in Python. Ich brauche detailliertere Antworten, um zu verstehen, warum es nicht funktioniert hat.asyncio
, was für mich funktioniert, auf C-Code hinauslaufen, der veranschaulicht, in was die Python-Syntax übersetzt wurde.