Nebenläufigkeit in Python: async und await verstehen
Was async und await wirklich bedeuten, wann asynchrone Programmierung sich lohnt – und wo du danach noch tiefer eintauchen kannst.
Früher oder später stößt fast jeder Python-Lernende auf die Schlüsselwörter async und await – und ist erst einmal verwirrt. Dieser Artikel erklärt die Grundidee der asynchronen Programmierung so, dass sie wirklich Klick macht. Wenn du danach eine ausführliche, praxisnahe Vertiefung auf Englisch suchst, empfehle ich dir den exzellenten Deep Dive des Schwesterblogs pykit.org: Async Python Demystified: A Practical Deep Dive into asyncio. Dort werden Event-Loop, Tasks und reale Anwendungsfälle Schritt für Schritt zerlegt.
Das Problem: Warten kostet Zeit
Stell dir vor, dein Programm soll 50 Webseiten abrufen. Bei jedem Abruf schickt dein Code eine Anfrage los und wartet dann – oft mehrere hundert Millisekunden – bis die Antwort eintrifft. Klassischer, synchroner Code macht das nacheinander: Anfrage 1, warten, Anfrage 2, warten, und so weiter. Die meiste Zeit tut dein Programm dabei nichts, es wartet nur auf das Netzwerk.
Genau hier setzt asynchrone Programmierung an. Statt untätig zu warten, kann dein Programm die Wartezeit nutzen, um schon die nächste Anfrage loszuschicken. Das nennt man Nebenläufigkeit (englisch concurrency).
Coroutinen: Funktionen, die pausieren können
Eine normale Funktion läuft von oben bis unten durch. Eine mit async def definierte Funktion ist dagegen eine sogenannte Coroutine – sie kann an bestimmten Stellen pausieren und die Kontrolle abgeben:
import asyncio
async def gruessen():
print("Hallo")
await asyncio.sleep(1) # hier wird pausiert
print("Welt")
asyncio.run(gruessen())Das Schlüsselwort await bedeutet sinngemäß: „Diese Operation dauert – warte hier, aber gib währenddessen anderen Aufgaben die Chance zu laufen." Wichtig: await funktioniert nur innerhalb einer async-Funktion.
Der Aha-Moment: mehrere Aufgaben gleichzeitig
Der eigentliche Gewinn zeigt sich, wenn mehrere Coroutinen parallel laufen. Mit asyncio.gather startest du sie alle und wartest gemeinsam auf das Ergebnis:
import asyncio
async def aufgabe(name, dauer):
print(f"{name} startet")
await asyncio.sleep(dauer)
print(f"{name} fertig nach {dauer}s")
return name
async def main():
ergebnisse = await asyncio.gather(
aufgabe("A", 2),
aufgabe("B", 1),
aufgabe("C", 3),
)
print("Alle fertig:", ergebnisse)
asyncio.run(main())Synchron würde dieser Ablauf 2 + 1 + 3 = 6 Sekunden dauern. Asynchron ist er nach rund 3 Sekunden fertig – so lange wie die langsamste Einzelaufgabe. Die Wartezeiten überlappen sich.
Wann lohnt sich async – und wann nicht?
Asynchrone Programmierung glänzt bei I/O-lastigen Aufgaben: Netzwerkanfragen, Datenbankzugriffe, das Lesen vieler Dateien. Überall dort, wo dein Programm auf etwas Externes wartet, kannst du die Wartezeit sinnvoll füllen.
Bei rechenintensiven Aufgaben – etwa komplizierten Berechnungen, die die CPU dauerhaft beschäftigen – bringt asyncio dagegen wenig. Hier gibt es keine Wartezeit zu überbrücken; dafür sind Prozesse oder Threads die bessere Wahl. Eine einzelne Coroutine, die nie await aufruft, blockiert sogar die gesamte Event-Loop.
Ein häufiger Stolperstein
Anfänger vergessen oft das await und wundern sich, warum „nichts passiert". Ruft man eine Coroutine ohne await auf, erhält man nur ein Coroutine-Objekt zurück – der Code im Inneren läuft nie. Merke: Eine Coroutine ist wie ein Rezept. Erst await (oder asyncio.run) kocht das Gericht tatsächlich.
Wie es weitergeht
Wenn du die Grundidee verstanden hast, lohnt sich der Sprung in die Tiefe: Wie funktioniert die Event-Loop genau? Wie behandelt man Fehler in mehreren Tasks? Wie kombiniert man asyncio mit echten HTTP-Bibliotheken? All das beantwortet der ausführliche englische Leitfaden auf pykit.org: Async Python Demystified: A Practical Deep Dive into asyncio. Eine sehr empfehlenswerte nächste Station auf deiner Codereise.