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.

Teilen

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.