Zum Inhalt springen
aviral gupta

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

asyncio: Coroutines und Tasks

Nach dieser Lektion schreiben Sie Coroutines, starten sie aus einem normalen Programm, überlappen Wartezeiten in einem Thread, erkennen blockierende Aufrufe und starten andere Programme, ohne zu blockieren.

Lektion 4 von 6 in A3 Nebenläufigkeit

Danach können Sie

  • Coroutines mit async def und await schreiben und mit asyncio.run starten
  • Coroutines mit create_task und gather nebenläufig ausführen und ihre Ergebnisse einsammeln
  • Erklären, wo die Event-Loop zwischen Tasks wechselt und warum ein blockierender Aufruf sie anhält
  • Aus einer Coroutine ein anderes Programm mit create_subprocess_exec starten und seine Ausgabe lesen
  1. Aufwärmen · Aufgabe 1 von 8

    Aufwärmen aus der letzten Lektion: Welcher Executor passt zu 300 URL-Prüfungen, die fast nur warten?

  2. Vorhersagen · Aufgabe 2 von 8

    Sagen Sie es vorher, bevor Sie weiterlesen. greet wird aufgerufen, aber nie mit await abgewartet. Was gibt das aus?

    import asyncio
    
    
    async def greet():
        print("hello")
        return 42
    
    
    result = greet()
    print(type(result).__name__)
    result.close()
  3. Üben · Aufgabe 3 von 8

    Setzen Sie die asyncio-Funktion ein, die eine Event-Loop startet, main zu Ende führt und das Ergebnis liefert.

    import asyncio
    
    
    async def main():
        await asyncio.sleep(0)
        return "ran"
    
    
    print(asyncio.____(main()))
    print(asyncio.(main()))
  4. Üben · Aufgabe 4 von 8

    create_task plant say("B") ein. Was gibt das aus?

    import asyncio
    
    
    async def say(word):
        print(word, end=" ")
    
    
    async def main():
        task = asyncio.create_task(say("B"))
        print("A", end=" ")
        await task
        print("C")
    
    
    asyncio.run(main())
  5. Üben · Aufgabe 5 von 8

    Ordnen Sie jedem Stück asyncio zu, was es tut.

  6. Üben · Aufgabe 6 von 8

    Die Coroutine startet ein zweites Python, das hi ausgibt. Nur die Standardausgabe läuft über eine Pipe. Was gibt das aus?

    import asyncio
    import sys
    
    
    async def main():
        process = await asyncio.create_subprocess_exec(
            sys.executable, "-c", "print('hi')",
            stdout=asyncio.subprocess.PIPE,
        )
        out, err = await process.communicate()
        print(out, err, process.returncode)
    
    
    asyncio.run(main())
  7. Denksport · Aufgabe 7 von 8

    Knobelaufgabe. Der langsame Job steht zuerst. Was gibt das aus?

    import asyncio
    
    
    async def job(name, delay):
        await asyncio.sleep(delay)
        print("done", name, end="; ")
        return name
    
    
    async def main():
        print(await asyncio.gather(job("slow", 0.02), job("fast", 0.01)))
    
    
    asyncio.run(main())
  8. Anwenden · Aufgabe 8 von 8

    Mini-Aufgabe, auf Ihrem eigenen Rechner. Schreiben Sie eine Coroutine fetch(name, delay), die mit await auf asyncio.sleep(delay) wartet und name.upper() liefert. Sammeln Sie in main drei Abrufe von je 0,1 s für "home", "about" und "blog" mit gather ein und geben Sie die Liste aus, zusammen damit, ob das ganze gather weniger als 0,2 s dauerte, gemessen mit time.perf_counter. Starten Sie es mit asyncio.run.

    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

Drei Abrufe, drei Wege

fetch spielt einen Seitenabruf, indem es mit await auf asyncio.sleep(0.1) wartet. Das Programm führt drei Abrufe auf drei Wegen aus, jeden unter einem eigenen asyncio.run: einzeln mit await abgewartet, an gather übergeben und mit create_task gestartet. Die Zeilen start und end zeigen, wann jede Coroutine die Kontrolle abgibt, und die Zeiten zeigen, wie die Wartezeiten überlappen. 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
import time
from collections.abc import Coroutine
from typing import Any


async def fetch(name: str) -> str:
    """Pretend to download a page: the await hands control back to the event loop."""
    print("  start", name)
    await asyncio.sleep(0.1)  # waiting on the network
    print("  end", name)
    return name.upper()


async def one_by_one(names: list[str]) -> list[str]:
    return [await fetch(name) for name in names]


async def all_at_once(names: list[str]) -> list[str]:
    return await asyncio.gather(*(fetch(name) for name in names))


async def with_tasks(names: list[str]) -> list[str]:
    tasks = [asyncio.create_task(fetch(name)) for name in names]
    print("  tasks created, none started yet")
    return [await task for task in tasks]


def timed(label: str, coro: Coroutine[Any, Any, list[str]]) -> None:
    print(label)
    start = time.perf_counter()
    result = asyncio.run(coro)
    print(f"  {result}, done in under 0.2 s: {time.perf_counter() - start < 0.2}")


if __name__ == "__main__":
    names = ["a", "b", "c"]
    timed("await one by one:", one_by_one(names))
    timed("gather:", all_at_once(names))
    timed("create_task:", with_tasks(names))

Ausführen mit

python main.py

Ausgabe

await one by one:
  start a
  end a
  start b
  end b
  start c
  end c
  ['A', 'B', 'C'], done in under 0.2 s: False
gather:
  start a
  start b
  start c
  end a
  end b
  end c
  ['A', 'B', 'C'], done in under 0.2 s: True
create_task:
  tasks created, none started yet
  start a
  start b
  start c
  end a
  end b
  end c
  ['A', 'B', 'C'], done in under 0.2 s: True
  • Einzeln abgewartet endet jeder Abruf, bevor der nächste startet: Drei Wartezeiten von 0,1 s dauern 0,3 s.
  • gather und create_task starten alle drei, bevor einer endet, also überlappen die Wartezeiten zu etwa 0,1 s.
  • Die Tasks starteten nicht beim Aufruf von create_task, sondern beim ersten await in with_tasks.
  • Alles lief in einem Thread: Gewechselt wurde nur bei await asyncio.sleep.

Übungen

Übung 1 von 3

Alle auf einmal laden

fetch_all(names, fetch) wartet mit await einen Abruf nach dem anderen ab, also addieren sich die Wartezeiten. Ändern Sie es so, dass alle fetch(name) mit asyncio.gather nebenläufig laufen, und liefern Sie weiterhin ein Dict von jedem Namen zu seinem Text in der Reihenfolge von names. fetch ist eine beliebige Coroutine-Funktion, die einen Namen nimmt. 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

    fetch(name) ohne await erzeugt eine Coroutine; ein Generator-Ausdruck (fetch(name) for name in names) erzeugt eine pro Name.

  2. Hinweis 2

    Entpacken Sie sie in gather: await asyncio.gather(*coroutines). Die Ergebnisse kommen in derselben Reihenfolge wie die Namen zurück.

  3. Hinweis 3

    dict(zip(names, texts)) verbindet jeden Namen mit seinem Text.

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_all(names: list[str], fetch: Callable[[str], Awaitable[str]]) -> dict[str, str]:
    """Run fetch(name) for every name concurrently; map each name to its text, in the order of names."""
    texts = await asyncio.gather(*(fetch(name) for name in names))
    return dict(zip(names, texts))
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_all(names: list[str], fetch: Callable[[str], Awaitable[str]]) -> dict[str, str]:
    """Run fetch(name) for every name concurrently; map each name to its text, in the order of names."""
    # Each await waits for one fetch before the next one starts.
    # Start them all together with asyncio.gather instead.
    return {name: await fetch(name) for name in names}

test_main.py

import asyncio

from main import fetch_all


def test_results():
    """Jeder Name wird auf seinen Text abgebildet"""

    async def fake_fetch(name):
        await asyncio.sleep(0)
        return name * 2

    got = asyncio.run(fetch_all(["a", "b"], fake_fetch))
    assert got == {"a": "aa", "b": "bb"}, f"fetch_all lieferte {got!r}"


def test_order_of_names():
    """Das Dict folgt der Reihenfolge von names, auch wenn ein späterer Abruf zuerst fertig wird"""

    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_fetches_overlap():
    """Alle drei Abrufe warten gleichzeitig"""
    running = 0
    peak = 0

    async def fake_fetch(name):
        nonlocal running, peak
        running += 1
        peak = max(peak, running)
        await asyncio.sleep(0)
        running -= 1
        return name

    asyncio.run(fetch_all(["a", "b", "c"], fake_fetch))
    assert peak == 3, f"höchstens {peak} Abruf(e) warteten gleichzeitig: starten Sie alle mit asyncio.gather"

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

Die Küche entblocken

brew_all startet einen Task pro Bestellung, aber brew ruft time.sleep auf, das die ganze Event-Loop blockiert: Jedes Getränk wird komplett zubereitet, bevor das nächste überhaupt beginnt. Ändern Sie brew so, dass die Getränke gleichzeitig ziehen und eine kürzere Zubereitung zuerst endet. Lassen Sie die Log-Einträge und Rückgabewerte, wie sie sind. 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

    Die Event-Loop kann Tasks nur an einem await wechseln, das pausiert. time.sleep gibt die Kontrolle nie ab.

  2. Hinweis 2

    asyncio.sleep(seconds) ist die Variante für await: Schreiben Sie await asyncio.sleep(seconds), statt time.sleep aufzurufen.

  3. Hinweis 3

    Ist time.sleep weg, wird die Zeile import time nicht mehr gebraucht.

Eine Lösung zeigen

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

import asyncio


async def brew(name: str, seconds: float, log: list[str]) -> str:
    """Pretend to brew a drink for some seconds; log when it starts and ends."""
    log.append(f"{name} start")
    await asyncio.sleep(seconds)  # suspends this task; the others run meanwhile
    log.append(f"{name} end")
    return f"{name} ready"


async def brew_all(orders: dict[str, float]) -> tuple[list[str], list[str]]:
    """Brew every order in its own task; return (results in order, log)."""
    log: list[str] = []
    tasks = [asyncio.create_task(brew(name, seconds, log)) for name, seconds in orders.items()]
    results = [await task for task in tasks]
    return results, log
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
import time


async def brew(name: str, seconds: float, log: list[str]) -> str:
    """Pretend to brew a drink for some seconds; log when it starts and ends."""
    log.append(f"{name} start")
    time.sleep(seconds)  # blocks the event loop: no other task can run meanwhile
    log.append(f"{name} end")
    return f"{name} ready"


async def brew_all(orders: dict[str, float]) -> tuple[list[str], list[str]]:
    """Brew every order in its own task; return (results in order, log)."""
    log: list[str] = []
    tasks = [asyncio.create_task(brew(name, seconds, log)) for name, seconds in orders.items()]
    results = [await task for task in tasks]
    return results, log

test_main.py

import asyncio

from main import brew_all


def test_results():
    """Jede Bestellung ist fertig, in der Reihenfolge der Bestellungen"""
    results, _ = asyncio.run(brew_all({"tea": 0.01, "coffee": 0}))
    assert results == ["tea ready", "coffee ready"], f"brew_all lieferte {results!r}"


def test_brews_overlap():
    """Kaffee beginnt, während der Tee noch zieht, und die kürzere Zubereitung endet zuerst"""
    _, log = asyncio.run(brew_all({"tea": 0.02, "coffee": 0.01}))
    assert log == ["tea start", "coffee start", "coffee end", "tea end"], f"das Log war {log!r}: blockiert brew die Event-Loop?"

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

Programme nebeneinander ausführen

Schreiben Sie run_python(code): Es startet das laufende Python, sys.executable, mit den Argumenten "-c" und code, die Standardausgabe über eine Pipe, und liefert seinen Exit-Status und seine dekodierte Ausgabe ohne den letzten Zeilenumbruch. Ändern Sie dann run_all so, dass alle Programme gleichzeitig laufen, mit den Ergebnissen in der Reihenfolge von snippets. Auf Ihrem Rechner zeigt python main.py eine Demo, 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

    process = await asyncio.create_subprocess_exec(sys.executable, "-c", code, stdout=asyncio.subprocess.PIPE) startet das Programm; jedes Argument ist ein eigener String.

  2. Hinweis 2

    out, _ = await process.communicate() wartet auf das Ende. out ist bytes: out.decode().rstrip("\n") liefert den Text, process.returncode den Exit-Status.

  3. Hinweis 3

    Übergeben Sie für run_all ein run_python(code) pro Snippet an asyncio.gather, wie in Übung 1.

Eine Lösung zeigen

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

import asyncio
import sys


async def run_python(code: str) -> tuple[int, str]:
    """Run code in a new Python process; return its exit status and its output without the last newline."""
    process = await asyncio.create_subprocess_exec(sys.executable, "-c", code, stdout=asyncio.subprocess.PIPE)
    out, _ = await process.communicate()
    assert process.returncode is not None  # set once the program has ended
    return process.returncode, out.decode().rstrip("\n")


async def run_all(snippets: list[str]) -> list[tuple[int, str]]:
    """Run every snippet at the same time; return the results in the order of snippets."""
    return await asyncio.gather(*(run_python(code) for code in snippets))


if __name__ == "__main__":
    print(asyncio.run(run_all(["print('hello')", "import sys; sys.exit(3)"])))
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
import sys


async def run_python(code: str) -> tuple[int, str]:
    """Run code in a new Python process; return its exit status and its output without the last newline."""
    # TODO: start sys.executable with the arguments "-c" and code, standard output piped;
    # await communicate(), then return the exit status and the decoded output.
    raise NotImplementedError


async def run_all(snippets: list[str]) -> list[tuple[int, str]]:
    """Run every snippet at the same time; return the results in the order of snippets."""
    # This waits for each program before it starts the next one.
    return [await run_python(code) for code in snippets]


if __name__ == "__main__":
    print(asyncio.run(run_all(["print('hello')", "import sys; sys.exit(3)"])))

test_main.py

import asyncio
import time

from main import run_all, run_python


def test_output():
    """run_python liefert Exit-Status 0 und das, was das Programm ausgegeben hat"""
    got = asyncio.run(run_python("print('hi')"))
    assert got == (0, "hi"), f"run_python lieferte {got!r}, erwartet war (0, 'hi')"


def test_exit_status():
    """Ein Programm, das mit sys.exit(3) endet, liefert Exit-Status 3"""
    got = asyncio.run(run_python("import sys; sys.exit(3)"))
    assert got == (3, ""), f"run_python lieferte {got!r}, erwartet war (3, '')"


def test_programs_overlap():
    """Drei Programme von je 0,5 s laufen gleichzeitig, und die Ergebnisse behalten ihre Reihenfolge"""
    snippets = [f"import time; time.sleep(0.5); print({n})" for n in range(3)]
    started = time.perf_counter()
    got = asyncio.run(run_all(snippets))
    took = time.perf_counter() - started
    assert got == [(0, "0"), (0, "1"), (0, "2")], f"run_all lieferte {got!r}"
    assert took < 1.4, f"run_all brauchte {took:.2f} s: starten Sie alle Programme, bevor Sie auf eines warten"

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

asyncio.run in einer Coroutine aufrufen

import asyncio


async def inner() -> int:
    return 1


async def main() -> None:
    print(asyncio.run(inner()))


asyncio.run(main())

Was Python ausgibt

RuntimeError: asyncio.run() cannot be called from a running event loop

Warum, und die Lösung

main läuft bereits in der Event-Loop, die das äußere asyncio.run gestartet hat, und asyncio.run weigert sich, im selben Thread eine zweite zu starten. Schreiben Sie in einer Coroutine await inner(). Behalten Sie ein einziges asyncio.run als Einstiegspunkt des Programms.

await vergessen

import asyncio


async def fetch_number() -> int:
    await asyncio.sleep(0)
    return 41


async def main() -> None:
    total = fetch_number() + 1
    print(total)


asyncio.run(main())

Was Python ausgibt

TypeError: unsupported operand type(s) for +: 'coroutine' and 'int'

Warum, und die Lösung

fetch_number() ohne await ist ein Coroutine-Objekt, nicht 41, und es läuft nie. Schreiben Sie total = await fetch_number() + 1: await bindet stärker als +, also läuft die Coroutine zuerst. Ein Typprüfer wie mypy meldet diese Zeile, bevor Sie sie ausführen.

Die Funktion statt einer Coroutine übergeben

import asyncio


async def main() -> None:
    print("hello")


asyncio.run(main)

Was Python ausgibt

TypeError: An asyncio.Future, a coroutine or an awaitable is required

Warum, und die Lösung

asyncio.run braucht etwas, worauf es mit await warten kann: die Coroutine, die main() liefert, nicht die Funktion main. Schreiben Sie asyncio.run(main()).

Eine ganze Befehlszeile als einen String übergeben

import asyncio


async def main() -> None:
    process = await asyncio.create_subprocess_exec("python -c print(1)")
    await process.communicate()


asyncio.run(main())

Was Python ausgibt

FileNotFoundError: [Errno 2] No such file or directory: 'python -c print(1)'

Warum, und die Lösung

create_subprocess_exec nutzt keine Shell, also trennt niemand den String an den Leerzeichen: Es sucht ein Programm, dessen Name der ganze String ist. Übergeben Sie das Programm und jedes Argument als eigenen String: create_subprocess_exec(sys.executable, "-c", "print(1)").

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

Coroutines laufen erst, wenn jemand sie mit await abwartet

Eine mit async def definierte Funktion ist eine Coroutine-Funktion. Ihr Aufruf führt nichts von ihrem Rumpf aus: Er liefert ein Coroutine-Objekt. Etwas muss dieses Objekt antreiben. In einer anderen Coroutine schreiben Sie await coro(); das führt sie aus und liefert ihren Rückgabewert. Aus normalem Code rufen Sie asyncio.run(main()) auf: Es startet eine Event-Loop, führt main zu Ende, liefert das Ergebnis und schließt die Loop. Nutzen Sie asyncio.run einmal, als Einstiegspunkt; solange im selben Thread eine andere Event-Loop läuft, lässt es sich nicht aufrufen, also nie aus einer Coroutine heraus.

Tasks überlappen das Warten

await coro() wartet auf coro, bevor Ihre nächste Zeile läuft; drei await-Ausdrücke nacheinander dauern also die Summe ihrer Wartezeiten. asyncio.create_task(coro()) packt die Coroutine in einen Task und plant ihn ein; er startet, sobald die aktuelle Coroutine die Kontrolle abgibt, und später warten Sie mit await auf sein Ergebnis. asyncio.gather(a(), b(), c()) plant alle ein und liefert ihre Ergebnisse in der Reihenfolge der Argumente, egal in welcher Reihenfolge sie fertig werden. Behalten Sie eine Referenz auf jeden Task: Die Event-Loop hält nur schwache Referenzen auf Tasks.

Ein Thread, Wechsel beim await

Die Event-Loop führt alle Tasks in einem Thread aus. Sie wechselt nur, wenn die laufende Coroutine an einem await pausiert, etwa bei await asyncio.sleep(0.1) oder beim Lesen aus dem Netzwerk. Code zwischen zwei await-Ausdrücken läuft ohne Unterbrechung, darin gibt es also kein Race; aber ein blockierender Aufruf wie time.sleep oder eine lange CPU-Schleife friert alle anderen Tasks ein, bis er zurückkehrt. Nutzen Sie die Variante für await (asyncio.sleep) oder geben Sie blockierende Arbeit mit await asyncio.to_thread(func, *args) an einen Thread oder an einen Prozess-Pool ab.

Andere Programme, ohne zu blockieren

await asyncio.create_subprocess_exec(program, *args, stdout=asyncio.subprocess.PIPE) startet ein Programm und liefert einen Process. Es gibt keine Shell: Programm und jedes Argument sind eigene Strings, und sys.executable nennt das Python, das gerade läuft. await process.communicate() liest die Pipes, bis das Programm endet, und liefert (stdout, stderr) als bytes, None für eine nicht angeforderte Pipe. process.returncode enthält danach den Exit-Status. Während des Wartens laufen andere Tasks. Ein Programm, das Sie aufgeben, beenden Sie mit process.kill() und danach await process.wait().

Quellen

Zuletzt geprüft am 29. September 2026