asyncio ist die Standardbibliothek für nebenläufige Programmierung mit Coroutinen in Python. Sie erlaubt, viele Aufgaben parallel zu koordinieren, ohne für jede einen eigenen Thread zu starten – ideal für Netzwerkverkehr, Datei- und API-Zugriffe. Dieser Artikel zeigt die Bausteine und die typischen Fehler.
Das Grundmodell
asyncio arbeitet mit einer Ereignisschleife (event loop). Aufgaben geben die Kontrolle freiwillig ab, wenn sie auf etwas warten müssen – etwa auf eine Antwort aus dem Netz. In dieser Zeit kann die Schleife eine andere Aufgabe weiterführen. Das ist kooperatives Multitasking: Der Wechsel passiert nur an await-Stellen, nicht mitten in einer Anweisung.
import asyncio
async def hole(url: str) -> str:
await asyncio.sleep(1) # hier könnte ein Netzaufruf stehen
return f"antwort von {url}"
async def main() -> None:
ergebnis = await hole("https://example.org")
print(ergebnis)
asyncio.run(main())
Wichtig: asyncio.run() ist der einzige Einstiegspunkt. Eine Coroutine, die man aufruft, ohne sie zu awaiten oder als Task zu starten, tut nichts – sie erzeugt nur eine Warnung („coroutine was never awaited“).
Nebenläufig statt nacheinander
Der eigentliche Gewinn entsteht erst, wenn mehrere Aufgaben gleichzeitig laufen:
async def main() -> None:
urls = [f"https://example.org/{i}" for i in range(10)]
# Sammlung: alles parallel starten, Reihenfolge bleibt erhalten
ergebnisse = await asyncio.gather(*(hole(u) for u in urls))
# TaskGroup (ab Python 3.11): bessere Fehlerbehandlung
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(hole(u)) for u in urls]
for t in tasks:
print(t.result())
# Mit Zeitlimit
try:
daten = await asyncio.wait_for(hole("https://example.org"), timeout=2.0)
except TimeoutError:
print("Zeitüberschreitung")
asyncio.gather(): startet mehrere Awaitables und liefert die Ergebnisse in der Reihenfolge der Eingabe. Mitreturn_exceptions=Truebricht ein Fehler nicht alles ab.asyncio.TaskGroup: seit Python 3.11 der bevorzugte Weg. Schlägt eine Aufgabe fehl, werden die anderen abgebrochen und die Fehler gesammelt alsExceptionGroupgeworfen. Das verhindert verwaiste Tasks.asyncio.wait_for(): erzwingt ein Zeitlimit. Ohne Zeitlimit kann eine hängende Verbindung das gesamte Programm blockieren.asyncio.as_completed(): liefert Ergebnisse in der Reihenfolge ihres Eintreffens – nützlich für Anzeigen.
Semaphore: Nebenläufigkeit begrenzen
Unbegrenzte Nebenläufigkeit ist ein häufiger Fehler. Wer tausend Verbindungen gleichzeitig öffnet, wird vom Zielsystem gedrosselt oder vom eigenen Dateideskriptor-Limit gestoppt.
import asyncio
async def begrenzt(tasks, limit=20):
sem = asyncio.Semaphore(limit)
async def einer(task):
async with sem:
return await task
return await asyncio.gather(*(einer(t) for t in tasks))
Häufige Fehler
- Blockierender Code in einer Coroutine. Ein
time.sleep(), eine synchrone Datenbankabfrage oder eine CPU-Schleife blockiert die gesamte Schleife – alle anderen Aufgaben warten. Richtig istawait asyncio.sleep()und für blockierende Fremdbibliothekenasyncio.to_thread()oder ein Executor. - Task-Referenz verlieren. Wird der Rückgabewert von
create_task()nicht festgehalten, kann die Aufgabe vom Garbage Collector entfernt werden. Tasks immer in einer Variablen oder Sammlung halten. - Zu viele offene Verbindungen. Ohne
Semaphoreoder Connection-Pool entstehen schnell tausende Sockets. - Ausnahmen in Hintergrundaufgaben verschwinden. Wird ein Task nie abgewartet, sieht niemand die Ausnahme. Das ist ein stiller Fehler – Tasks immer einsammeln und auswerten.
- Schleifen und synchrone blockierende Bibliotheken mischen. asyncio und Threads lassen sich verbinden, aber jedes Muster braucht Sorgfalt; im Zweifel eine Bibliothek wählen, die asyncio unterstützt.
Wann asyncio passt – und wann nicht
asyncio ist stark bei I/O-lastigen Aufgaben: viele gleichzeitige HTTP-Abfragen, WebSocket-Verbindungen, Datenbankzugriffe, Dateizugriffe. Auch als Grundlage von Webservern und Clients ist es weit verbreitet.
Für CPU-lastige Arbeit ist es das falsche Werkzeug: Eine Berechnung blockiert die Schleife, und es gibt keine echten parallelen Threads. Dafür braucht man multiprocessing oder native Erweiterungen. Und wer nur wenige, voneinander unabhängige Aufgaben hat, fährt mit Threads (concurrent.futures.ThreadPoolExecutor) oft einfacher und ohne async-Durchdringung der gesamten Codebasis.
Verwandte Grundlagen: Python: Funktionen und Module, Python: Exceptions, Nebenläufigkeit, Asynchron.