8.1. Kooperative Nebenläufigkeit¶
Das Scheduling-Modell von Asyncio ist kooperativ, nicht präemptiv. Diese Unterscheidung ist das mit Abstand wichtigste mentale Modell, auf dem der Rest dieses Abschnitts aufbaut, daher lohnt es sich, sie zu klären, bevor irgendwelcher Code erscheint.
8.1.1. Präemptiv vs. kooperativ¶
Ein präemptiver Scheduler — die Art, die ein Desktop-Betriebssystem verwendet, um viele Programme gleichzeitig laufen zu lassen — kann das gerade laufende Stück Code zu jedem Zeitpunkt anhalten und zu einem anderen umschalten. Der laufende Code muss dafür nichts Besonderes tun; der Scheduler unterbricht ihn. Das macht präemptives Scheduling sehr flexibel (kein einzelnes Stück Code kann die anderen aushungern, indem es langsam ist), bedeutet aber auch, dass jede gemeinsam genutzte Variable sorgfältig geschützt werden muss, denn der Wechsel könnte überall stattfinden — sogar mitten im Schreiben eines Werts oder beim Lesen einer Liste.
Ein kooperativer Scheduler kann nur an Stellen zwischen Code-Stücken umschalten, an denen das gerade laufende Stück die Kontrolle explizit zurückgibt. In Asyncio sind das jedes await und jeder Aufruf einer Koroutine, die intern yieldet (am häufigsten asyncio.sleep()). Zwischen zwei Awaits hat die laufende Koroutine die CPU für sich allein.
Daraus ergeben sich zwei Konsequenzen:
Eine Koroutine, die niemals awaitet, wird niemals pausiert. Wenn eine Koroutine in einer engen Schleife ohne
awaitdarin sitzt, monopolisiert sie den Scheduler und nichts anderes kommt voran. Die Lösung besteht darin, an einer sinnvollen Stelle der Schleifeawait asyncio.sleep_ms(0)(oder einen anderen wartenden Aufruf) einzufügen.Gemeinsam genutzter Zustand ist zwischen Awaits sicher. Zwei Koroutinen können sich nicht mitten in einer Operation überschneiden, die kein
awaitenthält. Die Art von Korruption, die entsteht, wenn die Präemption mitten in einer mehrstufigen Aktualisierung landet — ein Stück Code liest einen Wert, während ein anderes ihn gerade ändert — kann hier schlicht nicht auftreten. Eine Koordination zwischen Koroutinen ist weiterhin erforderlich, wenn mehrere von ihnen eine Ressource über Awaits hinweg teilen müssen, aber das Problem der Verschachtelung mitten in einer Zeile tritt nicht auf.
8.1.2. Die drei Schichten¶
Jedes Asyncio-Skript ist aus denselben drei Schichten aufgebaut. Die nächsten beiden Seiten behandeln sie im Detail; dies sind die Bezeichnungen, die man beim Lesen im Hinterkopf behalten sollte.
Koroutinen — Funktionen, die mit
async defdeklariert werden, jede eine in sich geschlossene Arbeitseinheit, die an den passenden Stellen awaitet. Die Python-Übersicht hat die Schlüsselwörterasync/awaiteingeführt; in Asyncio sind sie die Art, wie eine Koroutine an den Scheduler zurückgibt.Tasks — ein Wrapper, den
asyncio.create_task()um eine Koroutine legt, um sie nebenläufig zur aktuellen auszuführen. Die Anwendung erstellt typischerweise eine Handvoll Tasks für die langlaufenden Aufgaben (die Schnappschuss-Schleife, den Netzwerk-Client, den UART-Leser, …).Die Ereignisschleife — die Engine darunter, die im Auge behält, welche Koroutinen warten und welche bereit sind zu laufen, und bei jedem
awaitzwischen Tasks umschaltet. Die Anwendung schreibt die Schleife nicht; sie übergibt eine Top-Level-Koroutine anasyncio.run(), und die Schleife treibt von dort aus alles an.
Wenn die Anwendung auf diese Weise beschrieben wird — als ein kleiner Satz von Koroutinen, die von einer Ereignisschleife zusammengesetzt werden — wird Nebenläufigkeit zu einer Eigenschaft der Form des Programms, nicht zu etwas, das die Anwendung Schritt für Schritt verwalten muss.