async/await in Python: Nebenläufigkeit leicht gemacht
Seite 3: Catch me if you can: asynchrone Exceptions
Asynchrone Exceptions werden Task-übergreifend abgefangen, wie das folgende Listing zeigt. Dadurch ist das Abfangen von Exceptions zentral einfach realisierbar. Auch die Lesbarkeit ist dabei ausgezeichnet.
import asyncio
async def will_ex():
raise Exception("This is unavoidable")
async def main():
try:
await asyncio.create_task(will_ex())
except Exception as ex:
print(ex)
asyncio.run(main())
Listing 9: Asynchrone Exceptions werden Task-übergreifend abgefangen
Im Gegensatz zu Awaitable Objects
lassen sich Tasks mehrfach mit await
verwenden. Wie im nächsten Listing dargestellt, lässt sich auf diese Weise eine Reihe von Abläufen als Reaktion auf einen Einzelablauf auslösen. Der in Zeile 10 erzeugte Task wird als Parameter an die drei Tasks übergeben, die in Zeile 12 innerhalb der Schleife erzeugt werden. Sobald der Task t aus Zeile 10 beendet ist, fahren alle Koroutinen fort, die diesen Task abgewartet haben. Das erinnert konzeptuell an ein "notify-all"-Muster.
import asyncio
async def trigger(t, id):
s = await t
print(s, id)
async def main():
t = asyncio.create_task(asyncio.sleep(1, 'Hello'))
for i in range(3):
asyncio.create_task(trigger(t, i))
await t
asyncio.run(main())
Listing 10: Ein Task wird mehrfach von verschiedenen anderen Tasks erwartet
Zusammenführen mehrerer Ablaufstränge
Häufig möchte man mehrere nebenläufige Abläufe wieder zu einem synchronisieren, wie es bei einem Task über das Anwenden von await
möglich ist. Damit man mehrere Tasks nicht immer wieder manuell mit einer Schleife synchronisieren muss, steht die Funktion asyncio.wait
zur Verfügung (siehe nachfolgendes Listing). Ohne diese nützliche Funktion würde man immer wieder gleichen oder sehr ähnlichen Quelltext erzeugen. Übergibt man nun asyncio.wait
eine Coroutine, so wird sie automatisch in einen neu erzeugten Task gelegt. Das Ausführen geschieht dabei nebenläufig und startet unmittelbar. Mit dem zweiten Argument der Funktion kann man steuern, wie das Synchronisieren mit den anderen Tasks konkret auszuführen ist.
import asyncio
import time
async def x(i: float):
await asyncio.sleep(i)
return i
async def main():
print(f"started at {time.strftime('%X')}")
done, pending = await asyncio.wait([x(i) for i in range(10)], return_when="FIRST_COMPLETED")
done, pending = await asyncio.wait([x(i) for i in range(10)], return_when="ALL_COMPLETED")
done, pending = await asyncio.wait([x(i) for i in range(10)], return_when="FIRST_EXCEPTION")
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
Listing 11: Anwendung von asyncio.wait
Übergibt man FIRST_COMPLETED
, dann kehrt die Funktion zurück, sobald der erste der übergebenen Tasks zurückkehrt. Der zuerst zurückgekehrte Task ist dann in der Variable done
abgelegt, alle anderen noch nicht beendeten Tasks !!!befinden sich zu diesem Zeitpunkt in der Variablen pending
.
Übergibt man ALL_COMPLETED
, dann kehrt das erwartete asyncio.wait
erst zurück, wenn alle Tasks ihre Ausführung beendet haben. In dem Fall ist pending
leer und alle übergebenen Tasks sind in done
enthalten.
Tasks der Reihe nach abarbeiten
In Python steht eine besonders komfortable Funktion zur Verfügung: asyncio.as_completed
. Damit kann man innerhalb einer Schleife die Tasks in der Reihenfolge bearbeiten, in der die Tasks ihre Ausführung beenden. Das ist im folgenden Listing dargestellt:
import asyncio
import time
async def x(i: float):
await asyncio.sleep(i)
return i
async def main():
print(f"started at {time.strftime('%X')}")
for f in asyncio.as_completed([x(i) for i in range(10)]):
print(await f)
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
Listing 12: Anwendung von asyncio.as_completed