async/await in Python: Nebenläufigkeit leicht gemacht

Seite 3: Catch me if you can: asynchrone Exceptions

Inhaltsverzeichnis

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

Tasks mehrfach auslösen (Abb. 4)

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

Synchronisieren mehrerer Tasks (Abb. 5)

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.

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