Zum Inhalt springen
aviral gupta

// A3.5 · ca. 40 Min. · Vertiefung

TaskGroup, Timeouts und Abbruch

Nach dieser Lektion führen Sie zusammengehörige Tasks in einer TaskGroup aus, die den Rest abbricht, wenn einer scheitert, behandeln die entstehende ExceptionGroup, begrenzen Wartezeiten und lassen Abbrüche durch Ihren Aufräumcode hindurch.

Lektion 5 von 6 in A3 Nebenläufigkeit

Danach können Sie

  • Zusammengehörige Tasks in einer TaskGroup ausführen und ihre Ergebnisse lesen
  • Die ExceptionGroup einer scheiternden TaskGroup mit except* behandeln
  • Wartezeiten mit asyncio.timeout begrenzen und beim Abbruch eines Tasks richtig aufräumen
  • Nebenläufigkeit mit asyncio.Semaphore begrenzen und Tasks mit asyncio.Event benachrichtigen
  1. Aufwärmen · Aufgabe 1 von 9

    Aufwärmen aus der letzten Lektion: bad löst aus, während slow in gather noch wartet. Was gibt das aus?

    import asyncio
    
    
    async def slow():
        await asyncio.sleep(0.01)
        print("slow finished", end="; ")
    
    
    async def bad():
        raise ValueError("bad input")
    
    
    async def main():
        try:
            await asyncio.gather(slow(), bad())
        except ValueError as error:
            print("caught:", error, end="; ")
        await asyncio.sleep(0.02)
        print("end")
    
    
    asyncio.run(main())
  2. Vorhersagen · Aufgabe 2 von 9

    Sagen Sie es vorher, bevor Sie weiterlesen. Dieselben zwei Coroutines laufen jetzt in einer TaskGroup. Was gibt das aus?

    import asyncio
    
    
    async def slow():
        try:
            await asyncio.sleep(1)
            print("slow finished")
        except asyncio.CancelledError:
            print("slow cancelled", end="; ")
            raise
    
    
    async def bad():
        await asyncio.sleep(0)
        raise ValueError("bad input")
    
    
    async def main():
        try:
            async with asyncio.TaskGroup() as tg:
                tg.create_task(slow())
                tg.create_task(bad())
        except* ValueError as group:
            print("caught:", group.exceptions)
    
    
    asyncio.run(main())
  3. Üben · Aufgabe 3 von 9

    Setzen Sie die asyncio-Klasse ein, deren async with-Block auf jeden darüber erzeugten Task wartet.

    import asyncio
    
    
    async def work(n):
        await asyncio.sleep(0)
        return n * 10
    
    
    async def main():
        async with asyncio.____() as tg:
            task = tg.create_task(work(4))
        print(task.result())
    
    
    asyncio.run(main())
    async with asyncio.() as tg:
  4. Üben · Aufgabe 4 von 9

    Der sleep dauert weit länger als die Frist. Was gibt das aus?

    import asyncio
    
    
    async def main():
        try:
            async with asyncio.timeout(0.01):
                await asyncio.sleep(1)
                print("slept")
        except TimeoutError:
            print("timed out", end="; ")
        print("carry on")
    
    
    asyncio.run(main())
  5. Üben · Aufgabe 5 von 9

    Ordnen Sie jedem Werkzeug zu, was es tut.

  6. Üben · Aufgabe 6 von 9

    Fünf Tasks teilen sich eine Semaphore(2). peak hält fest, wie viele Jobs höchstens gleichzeitig darin waren. Was gibt das aus?

    import asyncio
    
    running = 0
    peak = 0
    
    
    async def job(sem):
        global running, peak
        async with sem:
            running += 1
            peak = max(peak, running)
            await asyncio.sleep(0.01)
            running -= 1
    
    
    async def main():
        sem = asyncio.Semaphore(2)
        async with asyncio.TaskGroup() as tg:
            for _ in range(5):
                tg.create_task(job(sem))
        print(peak, sem.locked())
    
    
    asyncio.run(main())
  7. Üben · Aufgabe 7 von 9

    Zwei Tasks warten auf das Event. Setzen Sie die Event-Methode ein, die beide weckt.

    import asyncio
    
    
    async def waiter(event, log):
        await event.wait()
        log.append("woke")
    
    
    async def main():
        event = asyncio.Event()
        log = []
        tasks = [asyncio.create_task(waiter(event, log)) for _ in range(2)]
        await asyncio.sleep(0)  # both waiters are now waiting
        event.____()
    event.()
  8. Denksport · Aufgabe 8 von 9

    Knobelaufgabe. Es gibt ein except TimeoutError innerhalb des Blocks und eins außerhalb. Was gibt das aus?

    import asyncio
    
    
    async def main():
        try:
            async with asyncio.timeout(0.01):
                try:
                    await asyncio.sleep(1)
                except TimeoutError:
                    print("inner", end="; ")
        except TimeoutError:
            print("outer")
    
    
    asyncio.run(main())
  9. Anwenden · Aufgabe 9 von 9

    Mini-Aufgabe, auf Ihrem eigenen Rechner. fetch(name, delay) wartet mit await auf asyncio.sleep(delay) und liefert name.upper(). Schreiben Sie fetch_or_none(name, delay, limit), das None liefert, wenn fetch länger als limit Sekunden braucht. Führen Sie es in main in einer TaskGroup für ("home", 0.01), ("slow", 1) und ("blog", 0.02) mit einer Frist von 0,05 s aus und geben Sie die Ergebnisse in dieser Reihenfolge aus.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Eine Gruppe von Downloads, ein Fehlschlag und eine Frist

fetch spielt einen Seitenabruf mit sleep; eine Seite, die nicht in DELAYS steht, löst KeyError aus, und ein abgebrochenes fetch meldet das und löst den Abbruch erneut aus. fetch_group führt seine Seiten in einer TaskGroup aus. main zeigt drei Fälle: Alle Seiten kommen an, eine Seite fehlt, und eine langsame Seite trifft auf einen Timeout von 0,1 s. Speichern Sie es als main.py und starten Sie python main.py (unter macOS und Linux python3 main.py) auf Ihrem Rechner.

main.py

import asyncio

# Seconds each page takes to "download"; a page not listed here does not exist.
DELAYS = {"home": 0.01, "about": 0.02, "blog": 0.03, "archive": 1.0}


async def fetch(name: str) -> str:
    """Pretend to download a page. A missing page raises KeyError."""
    try:
        await asyncio.sleep(DELAYS[name])
    except asyncio.CancelledError:
        print(f"  {name}: cancelled")
        raise  # clean-up done: let the cancellation continue
    return name.upper()


async def fetch_group(names: list[str]) -> list[str]:
    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(fetch(name)) for name in names]
    # Leaving the block means every task has finished.
    return [task.result() for task in tasks]


async def main() -> None:
    print("all pages:", await fetch_group(["home", "about", "blog"]))

    print("one page missing:")
    try:
        await fetch_group(["about", "missing"])
    except* KeyError as group:
        print("  failed:", group.exceptions)

    print("a page that is too slow:")
    try:
        async with asyncio.timeout(0.1):
            await fetch_group(["home", "archive"])
    except TimeoutError:
        print("  timed out after 0.1 s")


if __name__ == "__main__":
    asyncio.run(main())

Ausführen mit

python main.py

Ausgabe

all pages: ['HOME', 'ABOUT', 'BLOG']
one page missing:
  about: cancelled
  failed: (KeyError('missing'),)
a page that is too slow:
  archive: cancelled
  timed out after 0.1 s
  • Die fehlende Seite scheiterte sofort, also brach die Gruppe about ab, während es noch schlief.
  • except* KeyError bekam eine ExceptionGroup; group.exceptions enthält den einen KeyError.
  • home wurde innerhalb von 0,1 s fertig; archive schlief noch, also brach der Timeout es ab, und die Gruppe gab den Abbruch weiter.
  • fetch fängt CancelledError nur, um ihn zu melden, und löst ihn dann erneut aus, also funktionieren TaskGroup und Timeout weiter.

Übungen

Übung 1 von 3

Alles oder nichts

fetch_all nutzt gather: Scheitert ein Abruf, laufen die anderen weiter, und ein nackter ValueError kommt heraus. Schreiben Sie es mit einer asyncio.TaskGroup neu: ein Task pro Name, ein Dict von jedem Namen zu seinem Text in der Reihenfolge von names, und bei einem Fehlschlag werden die anderen Abrufe abgebrochen und eine ExceptionGroup ausgelöst. Führen Sie die Tests auf Ihrem Rechner aus.

Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    Öffnen Sie async with asyncio.TaskGroup() as tg: und erzeugen Sie die Tasks darin mit tg.create_task(fetch(name)).

  2. Hinweis 2

    Eine Dict-Comprehension {name: tg.create_task(fetch(name)) for name in names} hält jeden Task bei seinem Namen.

  3. Hinweis 3

    Nach dem Block ist jeder Task fertig: Bauen Sie das Ergebnis mit task.result(). Das Abbrechen und die ExceptionGroup übernimmt die TaskGroup selbst.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import asyncio
from collections.abc import Callable, Coroutine
from typing import Any


async def fetch_all(names: list[str], fetch: Callable[[str], Coroutine[Any, Any, str]]) -> dict[str, str]:
    """Fetch every name in a TaskGroup; map each name to its text, in the order of names.

    If a fetch fails, the others are cancelled and an ExceptionGroup is raised.
    """
    async with asyncio.TaskGroup() as tg:
        tasks = {name: tg.create_task(fetch(name)) for name in names}
    return {name: task.result() for name, task in tasks.items()}
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import asyncio
from collections.abc import Callable, Coroutine
from typing import Any


async def fetch_all(names: list[str], fetch: Callable[[str], Coroutine[Any, Any, str]]) -> dict[str, str]:
    """Fetch every name in a TaskGroup; map each name to its text, in the order of names.

    If a fetch fails, the others are cancelled and an ExceptionGroup is raised.
    """
    # gather leaves the other fetches running when one fails.
    # Create one task per name in an asyncio.TaskGroup instead.
    texts = await asyncio.gather(*(fetch(name) for name in names))
    return dict(zip(names, texts))

test_main.py

import asyncio

from main import fetch_all


def test_results():
    """Jeder Name wird auf seinen Text abgebildet, in der Reihenfolge von names"""

    async def fake_fetch(name):
        await asyncio.sleep(0.02 if name == "slow" else 0)
        return name.upper()

    got = asyncio.run(fetch_all(["slow", "fast"], fake_fetch))
    assert list(got.items()) == [("slow", "SLOW"), ("fast", "FAST")], f"fetch_all lieferte {got!r}"


def test_failure_is_a_group():
    """Ein scheiternder Abruf kommt als ExceptionGroup mit seinem ValueError heraus"""

    async def fake_fetch(name):
        await asyncio.sleep(0)
        if name == "bad":
            raise ValueError("bad page")
        return name

    try:
        asyncio.run(fetch_all(["ok", "bad"], fake_fetch))
        error = None
    except Exception as caught:
        error = caught
    assert isinstance(error, ExceptionGroup), f"fetch_all löste {error!r} aus, keine ExceptionGroup"
    assert [str(e) for e in error.exceptions] == ["bad page"], f"die Gruppe enthält {error.exceptions!r}"


def test_others_cancelled():
    """Scheitert ein Abruf, wird der langsame abgebrochen, bevor fetch_all auslöst"""
    cancelled = []

    async def fake_fetch(name):
        if name == "bad":
            await asyncio.sleep(0)
            raise ValueError("bad page")
        try:
            await asyncio.sleep(1)
        except asyncio.CancelledError:
            cancelled.append(name)
            raise
        return name

    async def run():
        try:
            await fetch_all(["slow", "bad"], fake_fetch)
        except Exception:
            pass
        return list(cancelled)

    seen = asyncio.run(run())
    assert seen == ["slow"], f"abgebrochen, als fetch_all auslöste: {seen!r}; nutzen Sie eine TaskGroup"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 3

Den Timeout an der richtigen Stelle fangen

fetch_each führt jeden Abruf in einer TaskGroup aus, jeden unter einem eigenen asyncio.timeout(limit), und soll für eine zu langsame Seite "timeout" liefern. Aber eine langsame Seite lässt die ganze Gruppe mit einer ExceptionGroup scheitern, die einen TimeoutError enthält. Korrigieren Sie fetch_one so, dass eine langsame Seite "timeout" ergibt und die anderen Seiten trotzdem ankommen. Führen Sie die Tests auf Ihrem Rechner aus.

Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    Innerhalb des async with asyncio.timeout-Blocks zeigt sich ein Timeout am await als CancelledError.

  2. Hinweis 2

    Der Timeout-Kontextmanager löst TimeoutError beim Verlassen des Blocks aus, also kann nur Code außerhalb des Blocks ihn fangen.

  3. Hinweis 3

    Setzen Sie try: um den ganzen Block async with asyncio.timeout(limit): und liefern Sie in except TimeoutError: "timeout".

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import asyncio
from collections.abc import Awaitable, Callable


async def fetch_one(fetch: Callable[[str], Awaitable[str]], name: str, limit: float) -> str:
    """Return fetch(name), or "timeout" if it takes longer than limit seconds."""
    try:
        async with asyncio.timeout(limit):
            return await fetch(name)
    except TimeoutError:  # raised as the async with block exits
        return "timeout"


async def fetch_each(names: list[str], fetch: Callable[[str], Awaitable[str]], limit: float) -> list[str]:
    """Fetch every name in a TaskGroup, each with its own time limit; results in the order of names."""
    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(fetch_one(fetch, name, limit)) for name in names]
    return [task.result() for task in tasks]
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import asyncio
from collections.abc import Awaitable, Callable


async def fetch_one(fetch: Callable[[str], Awaitable[str]], name: str, limit: float) -> str:
    """Return fetch(name), or "timeout" if it takes longer than limit seconds."""
    async with asyncio.timeout(limit):
        try:
            return await fetch(name)
        except TimeoutError:  # never true here: inside the block the fetch sees CancelledError
            return "timeout"


async def fetch_each(names: list[str], fetch: Callable[[str], Awaitable[str]], limit: float) -> list[str]:
    """Fetch every name in a TaskGroup, each with its own time limit; results in the order of names."""
    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(fetch_one(fetch, name, limit)) for name in names]
    return [task.result() for task in tasks]

test_main.py

import asyncio

from main import fetch_each

DELAYS = {"home": 0, "blog": 0.01, "archive": 1}


async def fake_fetch(name):
    await asyncio.sleep(DELAYS[name])
    return name.upper()


def test_all_in_time():
    """Seiten, die rechtzeitig ankommen, liefern ihren Text"""
    got = asyncio.run(fetch_each(["home", "blog"], fake_fetch, 0.5))
    assert got == ["HOME", "BLOG"], f"fetch_each lieferte {got!r}"


def test_slow_page_times_out():
    """Eine Seite, die länger als die Frist braucht, ergibt "timeout", und die anderen kommen trotzdem an"""
    try:
        got = asyncio.run(fetch_each(["home", "archive", "blog"], fake_fetch, 0.05))
    except Exception as error:
        got = error
    assert got == ["HOME", "timeout", "BLOG"], f"fetch_each ergab {got!r}: wo wird der TimeoutError ausgelöst?"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 3 von 3

Höchstens limit zugleich

fetch_limited startet alle Abrufe auf einmal, was einen Server überlasten könnte. Ändern Sie es so, dass höchstens limit Abrufe gleichzeitig laufen, mit einer asyncio.Semaphore, die sich alle teilen. Die Ergebnisse bleiben in der Reihenfolge von names. Auf Ihrem Rechner zeigt python main.py, wie die Abrufe je zu zweit starten, und python learnrun.py test führt die Tests aus.

Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    Legen Sie slots = asyncio.Semaphore(limit) einmal an, in fetch_limited, vor der TaskGroup.

  2. Hinweis 2

    Setzen Sie in fetch_one das await in async with slots:, damit ein Abruf seinen Platz hält, solange er läuft.

  3. Hinweis 3

    Eine Semaphore, die in fetch_one entsteht, wäre für jeden Abruf eine neue und begrenzte nichts.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import asyncio
from collections.abc import Awaitable, Callable


async def fetch_limited(names: list[str], fetch: Callable[[str], Awaitable[str]], limit: int) -> list[str]:
    """Fetch every name in a TaskGroup, at most limit at a time; results in the order of names."""
    slots = asyncio.Semaphore(limit)  # one semaphore, shared by every fetch

    async def fetch_one(name: str) -> str:
        async with slots:
            return await fetch(name)

    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(fetch_one(name)) for name in names]
    return [task.result() for task in tasks]


async def demo_fetch(name: str) -> str:
    print("start", name)
    await asyncio.sleep(0.1)
    print("end", name)
    return name.upper()


if __name__ == "__main__":
    print(asyncio.run(fetch_limited(["home", "about", "blog", "shop"], demo_fetch, 2)))
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import asyncio
from collections.abc import Awaitable, Callable


async def fetch_limited(names: list[str], fetch: Callable[[str], Awaitable[str]], limit: int) -> list[str]:
    """Fetch every name in a TaskGroup, at most limit at a time; results in the order of names."""
    # TODO: every fetch starts at once. Create one asyncio.Semaphore(limit)
    # here and hold it in fetch_one around the await.

    async def fetch_one(name: str) -> str:
        return await fetch(name)

    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(fetch_one(name)) for name in names]
    return [task.result() for task in tasks]


async def demo_fetch(name: str) -> str:
    print("start", name)
    await asyncio.sleep(0.1)
    print("end", name)
    return name.upper()


if __name__ == "__main__":
    print(asyncio.run(fetch_limited(["home", "about", "blog", "shop"], demo_fetch, 2)))

test_main.py

import asyncio

from main import fetch_limited


def counting_fetch():
    """A fake fetch that records how many calls are waiting at the same time."""
    state = {"running": 0, "peak": 0}

    async def fetch(name):
        state["running"] += 1
        state["peak"] = max(state["peak"], state["running"])
        await asyncio.sleep(0.01)
        state["running"] -= 1
        return name.upper()

    return fetch, state


def test_results_in_order():
    """Jeder Name liefert seinen Text, in der Reihenfolge von names"""
    fetch, _ = counting_fetch()
    got = asyncio.run(fetch_limited(["home", "about", "blog"], fetch, 2))
    assert got == ["HOME", "ABOUT", "BLOG"], f"fetch_limited lieferte {got!r}"


def test_limit_is_kept():
    """Mit limit 2 laufen nie mehr als 2 Abrufe gleichzeitig"""
    fetch, state = counting_fetch()
    asyncio.run(fetch_limited(["a", "b", "c", "d", "e"], fetch, 2))
    assert state["peak"] == 2, f"{state['peak']} Abrufe liefen gleichzeitig, erwartet waren höchstens 2: halten Sie eine gemeinsame Semaphore um jeden Abruf"


def test_limit_is_used():
    """Mit limit 3 laufen wirklich drei Abrufe gleichzeitig"""
    fetch, state = counting_fetch()
    asyncio.run(fetch_limited(["a", "b", "c", "d", "e"], fetch, 3))
    assert state["peak"] == 3, f"{state['peak']} Abrufe liefen gleichzeitig, erwartet waren 3: legen Sie die Semaphore einmal an, nicht in jedem Abruf"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Den Fehlschlag einer TaskGroup mit einfachem except fangen

import asyncio


async def bad() -> None:
    raise ValueError("bad input")


async def main() -> None:
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(bad())
    except ValueError as error:
        print("caught:", error)


asyncio.run(main())

Was Python ausgibt

ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)

Warum, und die Lösung

Eine TaskGroup löst immer eine ExceptionGroup aus, auch bei einem einzigen Fehlschlag, und except ValueError passt nicht auf eine Gruppe. Schreiben Sie except* ValueError as group: und lesen Sie die Fehler aus group.exceptions.

Einen Task hinzufügen, nachdem die Gruppe beendet ist

import asyncio


async def work(n: int) -> int:
    await asyncio.sleep(0)
    return n * 10


async def main() -> None:
    async with asyncio.TaskGroup() as tg:
        first = tg.create_task(work(1))
    second = tg.create_task(work(2))
    print(first.result(), await second)


asyncio.run(main())

Was Python ausgibt

RuntimeError: TaskGroup <TaskGroup entered> is finished

Warum, und die Lösung

Ist der async with-Block verlassen, nimmt die Gruppe keine neuen Tasks mehr an. Erzeugen Sie jeden Task innerhalb des Blocks, darunter eingerückt, oder öffnen Sie für spätere Arbeit eine neue TaskGroup.

asyncio.timeout mit einfachem with nutzen

import asyncio


async def main() -> None:
    with asyncio.timeout(0.1):
        await asyncio.sleep(0.01)
    print("done")


asyncio.run(main())

Was Python ausgibt

TypeError: 'asyncio.timeouts.Timeout' object does not support the context manager protocol

Warum, und die Lösung

asyncio.timeout liefert einen asynchronen Kontextmanager, ebenso asyncio.TaskGroup: Beide brauchen async with, innerhalb einer Coroutine. Die Fehlermeldung selbst schlägt die Korrektur vor.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

TaskGroup: Kein Task überlebt den Block

async with asyncio.TaskGroup() as tg: öffnet eine Gruppe; tg.create_task(coro()) fügt ihr einen Task hinzu. Das Verlassen des async with-Blocks wartet auf jeden Task der Gruppe, auch auf Tasks, die während des Wartens hinzukommen, also ist nach dem Block jedes task.result() bereit. Ist der Block verlassen, nimmt die Gruppe keine neuen Tasks mehr an. Anders als create_task und gather aus der letzten Lektion bindet eine TaskGroup die Tasks an einen Codeblock: Keiner läuft weiter oder wird vergessen, wenn der Block endet.

Ein Fehlschlag bricht den Rest ab

Scheitert ein Task der Gruppe zum ersten Mal mit einer anderen Exception als CancelledError, bricht die Gruppe ihre übrigen Tasks ab, und auch den Rumpf des async with, falls er noch läuft. Sind alle Tasks beendet, werden die Fehlschläge in einer ExceptionGroup zusammengefasst, die der Block auslöst. Ein einfaches except ValueError passt nicht auf eine ExceptionGroup. Nutzen Sie except* ValueError as group: Es passt auf die ValueErrors in der Gruppe, und group.exceptions enthält sie. Mehrere except*-Klauseln können je ihren eigenen Typ behandeln.

Timeouts sind Abbrüche

async with asyncio.timeout(0.5): begrenzt das Warten im Block. Ist die Frist abgelaufen, wird der aktuelle Task abgebrochen: Das laufende await löst CancelledError aus, und der Kontextmanager macht daraus beim Verlassen des Blocks TimeoutError, also fangen Sie TimeoutError außerhalb des Blocks. Ein Abbruch, ob durch task.cancel(), einen Timeout oder eine TaskGroup, kommt als CancelledError beim nächsten await an. Räumen Sie in finally auf, oder fangen Sie CancelledError und lösen Sie ihn erneut aus: Er erbt von BaseException, also verschluckt except Exception ihn nicht versehentlich.

Semaphoren begrenzen, Events melden

asyncio.Semaphore(3) lässt höchstens drei Tasks zugleich durch. async with sem: erwirbt sie beim Eintritt und gibt sie beim Verlassen frei: Jedes Erwerben senkt einen internen Zähler, jedes Freigeben erhöht ihn, und ein Task, der ihn bei null vorfindet, wartet, bis ein anderer freigibt. Legen Sie eine Semaphore an und teilen Sie sie; eine Semaphore pro Task begrenzt nichts. Mit asyncio.Event() warten Tasks auf eine Nachricht: await event.wait() blockiert, bis ein anderer Task event.set() aufruft, das alle Wartenden zugleich weckt. Ist das Event schon gesetzt, liefert wait() sofort True; event.is_set() prüft ohne zu warten.

Quellen

Zuletzt geprüft am 29. September 2026