8.15. Fallstricke¶
Genau die Muster, die asyncio angenehm machen – keine Präemption, explizite awaits – bringen einen eigenen Satz an tückischen Konstellationen mit sich. Diese Seite ist der Katalog derjenigen, die häufig genug auftreten, um es wert zu sein, sie zu kennen.
8.15.1. Das await vergessen¶
Der Aufruf einer async def-Funktion gibt ein Koroutinen-Objekt zurück. Er führt nicht den Rumpf der Funktion aus. Um sie tatsächlich auszuführen, muss die Koroutine awaitet oder in einen Task verpackt werden:
async def main():
send_request() # bug: returns the coroutine, does nothing
await send_request() # right: run it to completion
asyncio.create_task(send_request()) # right: run it concurrently
Der Fehler ist still – das Koroutinen-Objekt wird erzeugt, verworfen und nie ausgeführt. Die Anwendung läuft weiter, als hätte alles funktioniert. MicroPython protokolliert manchmal eine Warnung, dass eine Koroutine nie awaitet wurde; manchmal aber auch nicht. Prüfe an jeder Aufrufstelle, die wie ein Funktionsaufruf aussieht, auf fehlende awaits.
8.15.2. Enge Schleifen ohne await¶
Eine Koroutine, die in einer Schleife läuft und nie awaitet, monopolisiert die Ereignisschleife. Kein anderer Task kommt voran, bis die Schleife verlassen wird oder die Kontrolle abgibt:
async def counter():
n = 0
while True:
n += 1 # bug: starves the loop
Die Lösung ist ein Yield innerhalb der Schleife – normalerweise await asyncio.sleep_ms(0) –, damit andere bereite Tasks eine Chance zum Laufen bekommen. Rechenintensive Arbeit gehört ebenfalls in diese Form: Eine Bildverarbeitungsschleife, die pro Durchlauf Hunderte von Millisekunden benötigt, sollte mindestens einmal pro Durchlauf die Kontrolle abgeben, damit der Rest des Programms nicht stockt.
8.15.3. Das Verschlucken von CancelledError¶
Die Seite zum Abbruch hat dies bereits ausführlich behandelt. Es wird hier wiederholt, weil es die häufigste Ursache für „meine Anwendung fährt nicht herunter“ ist: Eine Koroutine fängt asyncio.CancelledError zu Aufräumzwecken ab und vergisst, sie erneut auszulösen. Der Task läuft weiter; der Aufrufer, der den Abbruch angefordert hat, hängt für immer und wartet darauf, dass er fertig wird. Löse die Ausnahme nach dem Aufräumen immer erneut aus, oder verwende einen try/finally-Block anstelle eines expliziten except.
8.15.5. await auf Modulebene¶
await ist nur innerhalb eines async def-Rumpfs gültig. Es auf Modulebene zu schreiben – außerhalb jeder Koroutine – ist ein Syntaxfehler:
# bug: not inside an async def
result = await fetch()
Die Lösung besteht darin, die Arbeit in eine Koroutine zu legen und sie vom asyncio.run()-Einstiegspunkt des Programms aus aufzurufen.
8.15.6. Mehrere asyncio.run-Aufrufe¶
MicroPython hat eine Ereignisschleife. asyncio.run() zweimal hintereinander aufzurufen – einmal für die Einrichtung, einmal für die Hauptarbeit – nutzt weiterhin dieselbe Schleife. Es von innerhalb einer laufenden Koroutine aufzurufen, ist ein Fehler: Die Schleife läuft bereits. Beide Fälle treten am häufigsten auf, wenn ein Skript organisch wächst und der Autor versucht, es durch das Hinzufügen weiterer run()-Aufrufe zu erweitern, anstatt die neue Arbeit in das bestehende main einzugliedern.
8.15.7. Verwendung von Event aus einem Interrupt¶
asyncio.Event.set() kann nur sicher von innerhalb der Ereignisschleife aufgerufen werden. Ein Aufruf aus einem GPIO-Interrupt-Handler ist eine Gefahr für Datenbeschädigung. Um einen Task aus einem Interrupt aufzuwecken, verwende stattdessen ThreadSafeFlag – die Seite dazu behandelt die Vorgehensweise.
8.15.8. Lange synchrone Aufrufe¶
Eine Koroutine kann auf asyncios eigene Warte-Primitive awaiten; alles andere, was sie aufruft, läuft synchron und blockiert die Schleife, bis es zurückkehrt. Ein 200 ms blockierendes time.sleep(), ein SD-Karten-Schreibvorgang, der 80 ms zum Leeren braucht, eine große JPEG-Kompression, ein csi.CSI.snapshot()-Aufruf – jeder davon hält die Ereignisschleife für seine gesamte Dauer fest. Die Lösung hängt vom Aufruf ab:
Für
time.sleep: ersetze es durchawait asyncio.sleepoderawait asyncio.sleep_ms.Für
csi.CSI.snapshot: verwende den asynchronen Snapshot-Wrapper, den die Erfassungsseite erstellt.Für lange Berechnungen (Bildverarbeitung, JPEG-Kodierung): nimm die Kosten in Kauf oder zerlege die Arbeit in Abschnitte, die zwischen den Durchläufen
awaiten.
Asyncio kann einen synchronen Aufruf nicht nicht-blockierend machen. Es kann nur andere Koroutinen laufen lassen, während etwas anderes awaitet.