Ein begrifflicher Überblick über "asyncio"
******************************************

Dieser HOWTO-Artikel möchte dir helfen, ein belastbares
Vorstellungsbild davon aufzubauen, wie "asyncio" im Grunde arbeitet –
und damit das Wie und Warum hinter den empfohlenen Mustern zu
verstehen.

Vielleicht bist du neugierig auf einige zentrale Begriffe von
"asyncio". Am Ende dieses Artikels wirst du diese Fragen mühelos
beantworten können:

* Was passiert hinter den Kulissen, wenn ein Objekt mit await erwartet
  wird?

* Wie unterscheidet "asyncio" zwischen einer Aufgabe, die keine CPU-
  Zeit braucht (etwa eine Netzwerkanfrage oder das Lesen einer Datei),
  und einer, die sie braucht (etwa die Berechnung von n-Fakultät)?

* Wie man eine asynchrone Variante einer Operation schreibt, etwa ein
  asynchrones Schlafen oder eine Datenbankanfrage.

Siehe auch:

  * Der Leitfaden, der diesen HOWTO-Artikel angeregt hat, von
    Alexander Nordin.

  * Diese ausführliche YouTube-Tutorialreihe zu "asyncio", erstellt
    von Łukasz Langa, Mitglied des Python-Kernteams.

  * 500 Lines or Less: A Web Crawler With asyncio Coroutines von A.
    Jesse Jiryu Davis und Guido van Rossum.


Ein begrifflicher Überblick, Teil 1: die obere Ebene
====================================================

In Teil 1 behandeln wir die wichtigsten Bausteine von "asyncio" auf
der oberen Ebene: die Ereignisschleife, Coroutine-Funktionen,
Coroutine-Objekte, Tasks und "await".


Ereignisschleife
----------------

Alles in "asyncio" geschieht in Bezug auf die Ereignisschleife. Sie
ist der Star der Vorstellung, arbeitet aber am liebsten hinter den
Kulissen und verwaltet und koordiniert Ressourcen. Sie ist wie ein
Orchesterdirigent. Ein Teil ihrer Macht ist ihr ausdrücklich
verliehen, aber ein Großteil ihrer Fähigkeit, Dinge zu bewegen, beruht
auf dem Respekt und der Mitarbeit ihrer Musiker.

Technischer gesagt: Die Ereignisschleife enthält eine Sammlung von
Aufträgen, die ausgeführt werden sollen. Manche Aufträge fügst du
unmittelbar selbst hinzu, manche mittelbar "asyncio". Die
Ereignisschleife nimmt einen Auftrag aus ihrem Arbeitsvorrat und ruft
ihn auf (oder „gibt ihm die Kontrolle“), ähnlich wie beim Aufruf einer
Funktion, und dieser Auftrag läuft dann. Sobald er anhält oder fertig
ist, gibt er die Kontrolle an die Ereignisschleife zurück. Die
Ereignisschleife wählt dann einen anderen Auftrag aus ihrem Vorrat und
ruft ihn auf. Du kannst dir die Sammlung von Aufträgen *ungefähr* als
Warteschlange vorstellen: Aufträge werden hinzugefügt und dann einer
nach dem anderen abgearbeitet, im Allgemeinen (aber nicht immer) in
dieser Reihenfolge. Dieser Vorgang wiederholt sich unbegrenzt, und die
Ereignisschleife kreist endlos weiter. Sind keine Aufträge zur
Ausführung mehr offen, ist die Ereignisschleife klug genug, zu ruhen
und keine CPU-Takte zu verschwenden. Sie kommt zurück, wenn es mehr zu
tun gibt – etwa wenn Ein- und Ausgabeoperationen abgeschlossen sind
oder Zeitgeber ablaufen.

Eine wirksame Ausführung beruht darauf, dass Aufträge gut teilen und
zusammenarbeiten. Ein gieriger Auftrag könnte die Kontrolle an sich
reißen und die anderen Aufträge verhungern lassen, was den ganzen
Ansatz der Ereignisschleife recht nutzlos machen würde.

   import asyncio

   # This creates an event loop and indefinitely cycles through
   # its collection of jobs.
   event_loop = asyncio.new_event_loop()
   event_loop.run_forever()


Asynchrone Funktionen und Coroutinen
------------------------------------

Dies ist eine einfache, langweilige Python-Funktion:

   def hello_printer():
       print(
           "Hi, I am a lowly, simple printer, though I have all I "
           "need in life -- \nfresh paper and my dearly beloved octopus "
           "partner in crime."
       )

Der Aufruf einer gewöhnlichen Funktion führt ihre Logik
beziehungsweise ihren Rumpf aus:

   >>> hello_printer()
   Hi, I am a lowly, simple printer, though I have all I need in life --
   fresh paper and my dearly beloved octopus partner in crime.

Das async def anstelle eines einfachen "def" macht dies zu einer
asynchronen Funktion (oder „Coroutine-Funktion“). Ihr Aufruf erzeugt
ein Coroutine-Objekt und gibt es zurück.

   async def loudmouth_penguin(magic_number: int):
       print(
        "I am a super special talking penguin. Far cooler than that printer. "
        f"By the way, my lucky number is: {magic_number}."
       )

Der Aufruf der asynchronen Funktion "loudmouth_penguin" führt die
print-Anweisung nicht aus. Er erzeugt stattdessen ein Coroutine-
Objekt:

   >>> loudmouth_penguin(magic_number=3)
   <coroutine object loudmouth_penguin at 0x104ed2740>

Die Begriffe „Coroutine-Funktion“ und „Coroutine-Objekt“ werden oft zu
„Coroutine“ verschmolzen. Das kann verwirren! In diesem Artikel
bezeichnet Coroutine ausdrücklich ein Coroutine-Objekt, genauer: eine
Instanz von "types.CoroutineType" (native Coroutine). Beachte, dass
Coroutinen auch als Instanzen von "collections.abc.Coroutine"
vorkommen können – ein Unterschied, der bei der Typprüfung eine Rolle
spielt.

Eine Coroutine steht für den Rumpf beziehungsweise die Logik der
Funktion. Eine Coroutine muss ausdrücklich gestartet werden. Nochmals:
Sie bloß zu erzeugen startet sie nicht. Bemerkenswert ist, dass die
Coroutine an verschiedenen Stellen innerhalb des Funktionsrumpfs
angehalten und fortgesetzt werden kann. Diese Fähigkeit zum Anhalten
und Fortsetzen ist es, die asynchrones Verhalten erst möglich macht!

Coroutinen und Coroutine-Funktionen wurden auf der Funktionalität von
*Generatoren* und *Generatorfunktionen* aufgebaut. Zur Erinnerung:
Eine Generatorfunktion ist eine Funktion, die "yield"-Anweisungen
enthält, wie diese hier:

   def get_random_number():
       # This would be a bad random number generator!
       print("Hi")
       yield 1
       print("Hello")
       yield 7
       print("Howdy")
       yield 4
       ...

Ähnlich wie bei einer Coroutine-Funktion führt der Aufruf einer
Generatorfunktion sie nicht aus. Er erzeugt stattdessen ein
Generatorobjekt:

   >>> get_random_number()
   <generator object get_random_number at 0x1048671c0>

Mit der integrierten Funktion "next()" kannst du zum nächsten "yield"
eines Generators vorrücken. Anders gesagt: Der Generator läuft und
hält dann an. Zum Beispiel:

   >>> generator = get_random_number()
   >>> next(generator)
   Hi
   1
   >>> next(generator)
   Hello
   7


Tasks
-----

Grob gesagt sind Tasks Coroutinen (keine Coroutine-Funktionen), die an
eine Ereignisschleife gebunden sind. Ein Task führt außerdem eine
Liste von Rückruffunktionen, deren Bedeutung gleich klar wird, wenn
wir "await" besprechen.

Einen Task zu erzeugen plant ihn automatisch zur Ausführung ein (indem
ein Rückruf, der ihn ausführt, in die Aufgabenliste der
Ereignisschleife – also ihre Sammlung von Aufträgen – eingetragen
wird). Der empfohlene Weg, Tasks zu erzeugen, ist
"asyncio.create_task()".

"asyncio" verknüpft Tasks für dich automatisch mit der
Ereignisschleife. Diese automatische Verknüpfung wurde um der
Einfachheit willen absichtlich in "asyncio" eingebaut. Ohne sie
müsstest du das Objekt der Ereignisschleife mitführen und an jede
Coroutine-Funktion übergeben, die Tasks erzeugen will – überflüssiger
Ballast in deinem Code.

   coroutine = loudmouth_penguin(magic_number=5)
   # This creates a Task object and schedules its execution via the event loop.
   task = asyncio.create_task(coroutine)

Vorhin haben wir die Ereignisschleife von Hand erzeugt und sie auf
unbegrenzten Betrieb gesetzt. In der Praxis ist es empfehlenswert (und
häufig zu sehen), "asyncio.run()" zu verwenden. Das kümmert sich um
die Verwaltung der Ereignisschleife und stellt sicher, dass die
übergebene Coroutine fertig ist, bevor es weitergeht. Viele asynchrone
Programme folgen zum Beispiel diesem Aufbau:

   import asyncio

   async def main():
       # Perform all sorts of wacky, wild asynchronous things...
       ...

   if __name__ == "__main__":
       asyncio.run(main())
       # The program will not reach the following print statement until the
       # coroutine main() finishes.
       print("coroutine main() is done!")

Es ist wichtig zu wissen, dass nicht der Task selbst in die
Ereignisschleife eingetragen wird, sondern nur ein Rückruf auf den
Task. Das spielt eine Rolle, wenn das von dir erzeugte Task-Objekt von
der Speicherbereinigung eingesammelt wird, bevor die Ereignisschleife
es aufruft. Betrachte zum Beispiel dieses Programm:

   async def hello():
       print("hello!")

   async def main():
       asyncio.create_task(hello())
       # Other asynchronous instructions which run for a while
       # and cede control to the event loop...
       ...

   asyncio.run(main())

Weil es keine Referenz auf das in Zeile 5 erzeugte Task-Objekt gibt,
*kann* es von der Speicherbereinigung eingesammelt werden, bevor die
Ereignisschleife es aufruft. Spätere Anweisungen in der Coroutine
"main()" geben die Kontrolle an die Ereignisschleife zurück, damit sie
andere Aufträge aufrufen kann. Wenn die Ereignisschleife den Task
irgendwann ausführen will, kann sie scheitern und feststellen, dass
das Task-Objekt nicht existiert! Das kann auch passieren, wenn eine
Coroutine zwar eine Referenz auf einen Task hält, aber fertig wird,
bevor dieser Task fertig ist. Beim Verlassen der Coroutine verlassen
lokale Variablen ihren Gültigkeitsbereich und können der
Speicherbereinigung zum Opfer fallen. In der Praxis arbeiten "asyncio"
und die Speicherbereinigung von Python ziemlich hart daran, dass so
etwas nicht passiert. Aber das ist kein Grund, leichtsinnig zu sein!


await
-----

"await" ist ein Python-Schlüsselwort, das üblicherweise auf eine von
zwei Weisen verwendet wird:

   await task
   await coroutine

In einem entscheidenden Punkt hängt das Verhalten von "await" davon
ab, welcher Art das erwartete Objekt ist.


Tasks erwarten
~~~~~~~~~~~~~~

Einen Task zu erwarten gibt die Kontrolle vom aktuellen Task oder der
aktuellen Coroutine an die Ereignisschleife ab. Beim Abgeben der
Kontrolle passieren ein paar wichtige Dinge. Wir verwenden das
folgende Codebeispiel zur Veranschaulichung:

   async def plant_a_tree():
       dig_the_hole_task = asyncio.create_task(dig_the_hole())
       await dig_the_hole_task

       # Other instructions associated with planting a tree.
       ...

Stell dir in diesem Beispiel vor, die Ereignisschleife hat die
Kontrolle an den Anfang der Coroutine "plant_a_tree()" übergeben. Wie
oben zu sehen, erzeugt die Coroutine einen Task und erwartet ihn dann.
Die Anweisung "await dig_the_hole_task" trägt einen Rückruf (der
"plant_a_tree()" fortsetzt) in die Rückrufliste des Objekts
"dig_the_hole_task" ein. Und dann gibt die Anweisung die Kontrolle an
die Ereignisschleife ab. Einige Zeit später übergibt die
Ereignisschleife die Kontrolle an "dig_the_hole_task", und der Task
erledigt, was er zu erledigen hat. Sobald der Task fertig ist, trägt
er seine verschiedenen Rückrufe in die Ereignisschleife ein – in
diesem Fall einen Aufruf, der "plant_a_tree()" fortsetzt.

Allgemein gesagt wird, sobald der erwartete Task ("dig_the_hole_task")
fertig ist, der ursprüngliche Task oder die ursprüngliche Coroutine
("plant_a_tree()") wieder in die Aufgabenliste der Ereignisschleife
eingetragen, um fortgesetzt zu werden.

Das ist ein einfaches, aber verlässliches Vorstellungsbild. In der
Praxis sind die Übergaben der Kontrolle etwas komplexer, aber nicht
viel. In Teil 2 gehen wir die Einzelheiten durch, die das möglich
machen.


Coroutinen erwarten
~~~~~~~~~~~~~~~~~~~

**Anders als bei Tasks gibt das Erwarten einer Coroutine die Kontrolle
nicht an die Ereignisschleife zurück!** Die Coroutine zuerst in einen
Task zu hüllen und diesen dann zu erwarten würde die Kontrolle
abgeben. Das Verhalten von "await coroutine" ist praktisch dasselbe
wie der Aufruf einer gewöhnlichen, synchronen Python-Funktion.
Betrachte dieses Programm:

   import asyncio

   async def coro_a():
      print("I am coro_a(). Hi!")

   async def coro_b():
      print("I am coro_b(). I sure hope no one hogs the event loop...")

   async def main():
      task_b = asyncio.create_task(coro_b())
      num_repeats = 3
      for _ in range(num_repeats):
         await coro_a()
      await task_b

   asyncio.run(main())

Die erste Anweisung in der Coroutine "main()" erzeugt "task_b" und
plant ihn über die Ereignisschleife zur Ausführung ein. Dann wird
"coro_a()" wiederholt erwartet. Die Kontrolle wird nie an die
Ereignisschleife abgegeben. Deshalb sehen wir die Ausgabe aller drei
"coro_a()"-Aufrufe vor der Ausgabe von "coro_b()":

   I am coro_a(). Hi!
   I am coro_a(). Hi!
   I am coro_a(). Hi!
   I am coro_b(). I sure hope no one hogs the event loop...

Ändern wir "await coro_a()" zu "await asyncio.create_task(coro_a())",
ändert sich das Verhalten. Die Coroutine "main()" gibt mit dieser
Anweisung die Kontrolle an die Ereignisschleife ab. Die
Ereignisschleife arbeitet dann ihren Arbeitsvorrat durch, ruft
"task_b" und danach den Task auf, der "coro_a()" umhüllt, und setzt
erst dann die Coroutine "main()" fort.

   I am coro_b(). I sure hope no one hogs the event loop...
   I am coro_a(). Hi!
   I am coro_a(). Hi!
   I am coro_a(). Hi!

Dieses Verhalten von "await coroutine" kann viele Leute aufs Glatteis
führen! Dieses Beispiel zeigt, wie der ausschließliche Gebrauch von
"await coroutine" unbeabsichtigt die Kontrolle von anderen Tasks
abziehen und die Ereignisschleife damit praktisch lahmlegen kann.
"asyncio.run()" kann dir helfen, solche Fälle zu erkennen – über das
Flag "debug=True", das den Debug-Modus einschaltet. Unter anderem
protokolliert er alle Coroutinen, die die Ausführung 100 ms oder
länger an sich reißen.

Der Entwurf gibt absichtlich etwas begriffliche Klarheit beim Gebrauch
von "await" für bessere Leistung auf. Jedes Mal, wenn ein Task
erwartet wird, muss die Kontrolle den gesamten Aufrufstapel hinauf an
die Ereignisschleife übergeben werden. Dann muss die Ereignisschleife
ihren inneren Zustand verwalten und ihre Verarbeitungslogik
durchlaufen, um den nächsten Auftrag fortzusetzen. Das klingt
vielleicht nach wenig, aber in einem großen Programm mit vielen
"await"-Anweisungen kann sich dieser Aufwand zu einer nicht zu
vernachlässigenden Leistungsbremse summieren.


Ein begrifflicher Überblick, Teil 2: das Innenleben
===================================================

Teil 2 geht ins Detail, welche Mechanismen "asyncio" verwendet, um den
Kontrollfluss zu lenken. Hier geschieht die Magie. Aus diesem
Abschnitt gehst du mit dem Wissen heraus, was "await" hinter den
Kulissen tut und wie du deine eigenen asynchronen Operatoren baust.


Das Innenleben von Coroutinen
-----------------------------

"asyncio" nutzt vier Bestandteile von Python, um die Kontrolle
weiterzugeben.

"coroutine.send(arg)" ist die Methode, mit der eine Coroutine
gestartet oder fortgesetzt wird. War die Coroutine angehalten und wird
jetzt fortgesetzt, wird das Argument "arg" als Rückgabewert der
"yield"-Anweisung hineingegeben, die sie ursprünglich angehalten hat.
Wird die Coroutine zum ersten Mal verwendet (und nicht fortgesetzt),
muss "arg" "None" sein.

   class Rock:
       def __await__(self):
           value_sent_in = yield 7
           print(f"Rock.__await__ resuming with value: {value_sent_in}.")
           return value_sent_in

   async def main():
       print("Beginning coroutine main().")
       rock = Rock()
       print("Awaiting rock...")
       value_from_rock = await rock
       print(f"Coroutine received value: {value_from_rock} from rock.")
       return 23

   coroutine = main()
   intermediate_result = coroutine.send(None)
   print(f"Coroutine paused and returned intermediate value: {intermediate_result}.")

   print(f"Resuming coroutine and sending in value: 42.")
   try:
       coroutine.send(42)
   except StopIteration as e:
       returned_value = e.value
   print(f"Coroutine main() finished and provided value: {returned_value}.")

yield hält die Ausführung wie üblich an und gibt die Kontrolle an den
Aufrufer zurück. Im obigen Beispiel wird das "yield" in Zeile 3 von
"... = await rock" in Zeile 11 aufgerufen. Allgemeiner gesagt ruft
"await" die Methode "__await__()" des gegebenen Objekts auf. "await"
tut außerdem noch etwas sehr Besonderes: Es gibt alle
"yield"-Ereignisse, die es empfängt, die Aufrufkette hinauf weiter. In
diesem Fall also zurück an "... = coroutine.send(None)" in Zeile 16.

Die Coroutine wird über den Aufruf "coroutine.send(42)" in Zeile 21
fortgesetzt. Die Coroutine macht dort weiter, wo sie in Zeile 3 mit
"yield" angehalten hat, und führt die restlichen Anweisungen ihres
Rumpfs aus. Wenn eine Coroutine fertig ist, löst sie eine
"StopIteration"-Ausnahme aus, an deren Attribut "value" der
Rückgabewert hängt.

Dieser Ausschnitt erzeugt diese Ausgabe:

   Beginning coroutine main().
   Awaiting rock...
   Coroutine paused and returned intermediate value: 7.
   Resuming coroutine and sending in value: 42.
   Rock.__await__ resuming with value: 42.
   Coroutine received value: 42 from rock.
   Coroutine main() finished and provided value: 23.

Es lohnt sich, hier einen Moment innezuhalten und sicherzustellen,
dass du den verschiedenen Wegen gefolgt bist, auf denen Kontrollfluss
und Werte weitergegeben wurden. Es wurden viele wichtige Ideen
behandelt, und es lohnt sich, dass dein Verständnis gefestigt ist.

Der einzige Weg, aus einer Coroutine ein yield zu machen (also die
Kontrolle wirksam abzugeben), ist, ein Objekt mit "await" zu erwarten,
das in seiner "__await__"-Methode ein "yield" ausführt. Das klingt für
dich vielleicht sonderbar. Du denkst vielleicht:

   1. What about a "yield" directly within the coroutine function? The
   coroutine function becomes an async generator function, a different
   beast entirely.

   2. What about a yield from within the coroutine function to a
   (plain) generator? That causes the error: "SyntaxError: yield from
   not allowed in a coroutine." This was intentionally designed for
   the sake of simplicity -- mandating only one way of using
   coroutines. Despite that, "yield from" and "await" effectively do
   the same thing. Initially "yield" was barred as well, but was re-
   accepted to allow for async generators.


Futures
-------

Ein Future ist ein Objekt, das den Stand und das Ergebnis einer
Berechnung darstellen soll. Der Begriff spielt auf die Vorstellung von
etwas an, das noch kommt oder noch nicht geschehen ist, und das Objekt
ist ein Weg, dieses Etwas im Auge zu behalten.

Ein Future hat ein paar wichtige Attribute. Eines ist sein Zustand,
der entweder „pending“, „cancelled“ oder „done“ sein kann. Ein anderes
ist sein Ergebnis, das gesetzt wird, wenn der Zustand auf done
wechselt. Anders als eine Coroutine stellt ein Future nicht die
eigentliche zu erledigende Berechnung dar. Es stellt vielmehr den
Stand und das Ergebnis dieser Berechnung dar, ein wenig wie eine
Statusampel (rot, gelb oder grün) oder eine Anzeige.

"asyncio.Task" ist eine Unterklasse von "asyncio.Future", um diese
verschiedenen Fähigkeiten zu bekommen. Im vorigen Abschnitt stand,
dass Tasks eine Liste von Rückrufen speichern, was nicht ganz genau
war. Tatsächlich setzt die Klasse "Future" diese Logik um, und "Task"
erbt sie.

Futures lassen sich auch unmittelbar verwenden (nicht über Tasks).
Tasks kennzeichnen sich selbst als fertig, wenn ihre Coroutine
abgeschlossen ist. Futures sind viel vielseitiger und werden als
fertig gekennzeichnet, wenn du es sagst. So sind sie die flexible
Schnittstelle, mit der du deine eigenen Bedingungen fürs Warten und
Fortsetzen festlegen kannst.


Ein selbstgebautes asyncio.sleep
--------------------------------

Wir gehen ein Beispiel dafür durch, wie du mit einem Future deine
eigene Variante eines asynchronen Schlafens ("async_sleep") bauen
könntest, die "asyncio.sleep()" nachbildet.

Dieser Ausschnitt meldet ein paar Tasks bei der Ereignisschleife an
und erwartet dann den von "asyncio.create_task" erzeugten Task, der
die Coroutine "async_sleep(3)" umhüllt. Wir wollen, dass dieser Task
erst fertig wird, nachdem drei Sekunden vergangen sind, ohne aber
andere Tasks am Laufen zu hindern.

   async def other_work():
       print("I like work. Work work.")

   async def main():
       # Add a few other tasks to the event loop, so there's something
       # to do while asynchronously sleeping.
       work_tasks = [
           asyncio.create_task(other_work()),
           asyncio.create_task(other_work()),
           asyncio.create_task(other_work())
       ]
       print(
           "Beginning asynchronous sleep at time: "
           f"{datetime.datetime.now().strftime("%H:%M:%S")}."
       )
       await asyncio.create_task(async_sleep(3))
       print(
           "Done asynchronous sleep at time: "
           f"{datetime.datetime.now().strftime("%H:%M:%S")}."
       )
       # asyncio.gather effectively awaits each task in the collection.
       await asyncio.gather(*work_tasks)

Unten verwenden wir einen Future, um eigens zu steuern, wann dieser
Task als fertig gekennzeichnet wird. Wird "future.set_result()" (die
Methode, die diesen Future als fertig kennzeichnet) nie aufgerufen,
wird dieser Task nie fertig. Wir haben außerdem die Hilfe eines
weiteren Tasks in Anspruch genommen – den wir gleich sehen –, der
überwacht, wie viel Zeit vergangen ist, und entsprechend
"future.set_result()" aufruft.

   async def async_sleep(seconds: float):
       future = asyncio.Future()
       time_to_wake = time.time() + seconds
       # Add the watcher-task to the event loop.
       watcher_task = asyncio.create_task(_sleep_watcher(future, time_to_wake))
       # Block until the future is marked as done.
       await future

Unten verwenden wir ein recht nacktes Objekt "YieldToEventLoop()", um
aus seiner "__await__"-Methode ein "yield" zu machen und damit die
Kontrolle an die Ereignisschleife abzugeben. Das ist praktisch
dasselbe wie der Aufruf von "asyncio.sleep(0)", aber dieser Weg ist
klarer – und es wäre ja auch etwas geschummelt, "asyncio.sleep" zu
verwenden, wenn man zeigt, wie man es umsetzt!

Wie üblich kreist die Ereignisschleife durch ihre Tasks, gibt ihnen
die Kontrolle und bekommt sie zurück, wenn sie anhalten oder fertig
sind. Der "watcher_task", der die Coroutine "_sleep_watcher(...)"
ausführt, wird einmal je vollem Umlauf der Ereignisschleife
aufgerufen. Bei jeder Fortsetzung prüft er die Zeit, und wenn noch
nicht genug vergangen ist, hält er wieder an und gibt die Kontrolle an
die Ereignisschleife zurück. Sobald genug Zeit vergangen ist,
kennzeichnet "_sleep_watcher(...)" den Future als fertig und beendet
sich, indem es seine endlose "while"-Schleife verlässt. Da dieser
Hilfstask nur einmal je Umlauf der Ereignisschleife aufgerufen wird,
hättest du recht mit der Feststellung, dass dieses asynchrone Schlafen
*mindestens* drei Sekunden schläft und nicht genau drei Sekunden.
Beachte, dass das auch für "asyncio.sleep" gilt.

   class YieldToEventLoop:
       def __await__(self):
           yield

   async def _sleep_watcher(future, time_to_wake):
       while True:
           if time.time() >= time_to_wake:
               # This marks the future as done.
               future.set_result(None)
               break
           else:
               await YieldToEventLoop()

Hier die Ausgabe des vollständigen Programms:

   $ python custom-async-sleep.py
   Beginning asynchronous sleep at time: 14:52:22.
   I like work. Work work.
   I like work. Work work.
   I like work. Work work.
   Done asynchronous sleep at time: 14:52:25.

Du findest diese Umsetzung des asynchronen Schlafens vielleicht
unnötig verwickelt. Und das war sie auch. Das Beispiel sollte die
Vielseitigkeit von Futures an einem einfachen Fall zeigen, der sich
für komplexere Bedürfnisse nachbauen lässt. Zum Vergleich: Du könntest
es auch ohne Futures umsetzen, etwa so:

   async def simpler_async_sleep(seconds):
       time_to_wake = time.time() + seconds
       while True:
           if time.time() >= time_to_wake:
               return
           else:
               await YieldToEventLoop()

Aber das ist erst einmal alles. Hoffentlich bist du nun bereit,
zuversichtlicher in die asynchrone Programmierung einzutauchen oder
dir fortgeschrittene Themen im "übrigen Teil der Dokumentation"
anzusehen.
