Aufwärmen · Aufgabe 1 von 8
// 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.
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
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()Ü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()))Ü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())Üben · Aufgabe 5 von 8
Ordnen Sie jedem Stück asyncio zu, was es tut.
Ü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())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())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.pyAusgabe
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
Hinweis 1
fetch(name) ohne await erzeugt eine Coroutine; ein Generator-Ausdruck (fetch(name) for name in names) erzeugt eine pro Name.
Hinweis 2
Entpacken Sie sie in gather: await asyncio.gather(*coroutines). Die Ergebnisse kommen in derselben Reihenfolge wie die Namen zurück.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.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
Hinweis 1
Die Event-Loop kann Tasks nur an einem await wechseln, das pausiert. time.sleep gibt die Kontrolle nie ab.
Hinweis 2
asyncio.sleep(seconds) ist die Variante für await: Schreiben Sie await asyncio.sleep(seconds), statt time.sleep aufzurufen.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.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
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.
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.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHä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 loopWarum, 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 requiredWarum, 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.