Files
it-know-how/python/Asynchron in Python.md
T
2026-03-27 12:58:01 +01:00

12 KiB
Executable File
Raw Blame History

Hier eine Einführung in „asynchron“ in der Python-Programmierung, für jemanden ohne Vorkenntnisse in diesem Bereich.


1. Intuitive Vorstellung: Was heißt „asynchron“?

Stell dir vor, du kochst:

  • Du stellst Wasser auf den Herd (es braucht Zeit, bis es kocht).
  • Während du wartest, schneidest du Gemüse, bereitest Sauce vor usw.
  • Du blockierst nicht deine Zeit, indem du nur auf den Topf starrst.

Asynchron bedeutet in der Programmierung:
Dein Programm kann etwas starten, das länger dauert (z.B. eine Netzwerkabfrage), und während es darauf wartet, andere Dinge erledigen, statt „untätig“ zu blockieren.


2. Grundbegriffe: synchron vs. asynchron

2.1 Synchron (blockierend)

Synchroner, „klassischer“ Code:

  • Befehl A wird ausgeführt.
  • Erst wenn A fertig ist, wird B ausgeführt.
  • Wenn A lange wartet (z.B. auf eine Antwort aus dem Internet), steht das ganze Programm an dieser Stelle still.

Beispiel (synchron, blockierend):

import time

print("Starte")
time.sleep(5)  # wartet 5 Sekunden  Programm ist blockiert
print("Fertig")

Während time.sleep(5) läuft, kann das Programm nichts anderes tun.

2.2 Asynchron (nicht-blockierend innerhalb eines Ablaufs)

Asynchroner Code versucht:

  • Langsame Operationen (z.B. Netzwerk, Festplatte, Datenbank) so zu starten,
  • und während sie „laufen“, andere Aufgaben zu bearbeiten.

In Python machst du das typischerweise mit async und await.

Sehr vereinfachtes Beispiel:

import asyncio

async def aufgabe(name, dauer):
    print(f"{name} gestartet")
    await asyncio.sleep(dauer)  # nicht-blockierend warten
    print(f"{name} fertig")

async def main():
    # Zwei Aufgaben (Tasks) gleichzeitig laufen lassen
    task1 = asyncio.create_task(aufgabe("A", 2))
    task2 = asyncio.create_task(aufgabe("B", 2))

    await task1
    await task2

asyncio.run(main())

Typischer Ablauf:

  • „A gestartet“
  • „B gestartet“
  • (2 Sekunden vergehen)
  • „A fertig“
  • „B fertig“

Beide Aufgaben „warten gleichzeitig“, und die Zeit überlappt sich.


3. Wichtige Abgrenzungen: Begriffe, die oft durcheinandergehen

3.1 Nebenläufigkeit (Concurrency) vs. Parallelität

  • Nebenläufigkeit (Concurrency): Mehrere Aufgaben werden so organisiert, dass sie scheinbar gleichzeitig laufen, indem man schnell zwischen ihnen hin- und herschaltet.
  • Parallelität (Parallelism): Mehrere Aufgaben laufen wirklich gleichzeitig auf mehreren CPU-Kernen.

Asynchrones Programmieren in Python (asyncio) ist in erster Linie ein Werkzeug für Nebenläufigkeit, nicht zwingend für echte Parallelität.

3.2 Threads vs. Async

  • Threads:
    • Betriebssystem-Fäden (OS-Threads).
    • Können an verschiedenen CPU-Kernen parallel laufen.
    • Schwerer zu testen, zu debuggen (Race Conditions, Deadlocks).
  • Async (z.B. asyncio in Python):
    • Läuft typischerweise in einem Thread.
    • Nutzt einen Event Loop, um zwischen Aufgaben zu wechseln, wenn sie gerade warten.
    • Sehr gut geeignet, wenn viele Aufgaben hauptsächlich I/O-lastig sind (Netzwerk, Dateien).

Kurz:

  • Viele Netzwerk-Anfragen gleichzeitig? → Async kann ideal sein.
  • Viel CPU-Berechnung (z.B. Bildverarbeitung)? → Threads oder Prozesse (Multiprocessing) sind oft sinnvoller.

3.3 Blocking vs. Non-blocking I/O

  • Blockierend: „Lies aus dem Netzwerk“ der Code bleibt stehen, bis Daten da sind.
  • Non-blockierend: „Lies aus dem Netzwerk, aber wenn gerade nichts da ist, mach solange andere Aufgaben.“

Asynchrones Python nutzt non-blocking I/O und einen Event Loop, um viele solcher Operationen gleichzeitig zu verwalten.


4. Asynchron in Python konkret: asyncio, async, await

4.1 Historischer Kontext

  • Vor Python 3.4 gab es asyncio nur als externes Paket.
  • Ab Python 3.5 wurden die Schlüsselwörter async und await eingeführt und haben das Arbeiten mit Async deutlich angenehmer gemacht.

4.2 Zentrale Begriffe

  • Coroutine: eine Funktion, die „angehalten“ und später fortgesetzt werden kann. In Python: definiert mit async def.
  • Event Loop: eine Schleife, die:
    • Aufgaben plant,
    • sie laufen lässt, bis sie warten müssen (z.B. auf I/O),
    • dann anderen Aufgaben CPU-Zeit gibt.
  • Task: eine geplante Coroutine, die vom Event Loop verwaltet wird.
  • Future: ein Platzhalter für ein Ergebnis, das noch nicht fertig ist.

4.3 Einfaches Beispiel: Event Loop und Coroutines

import asyncio

async def hallo():
    print("Hallo...")
    await asyncio.sleep(1)  # simuliert I/O-Wartezeit
    print("...Welt!")

async def main():
    await hallo()

asyncio.run(main())
  • hallo() ist eine Coroutine.
  • await asyncio.sleep(1) bedeutet: „warte 1 Sekunde, aber blockiere nicht den Event Loop“.

5. Welche Probleme werden durch asynchrones Programmieren gelöst?

5.1 Viele gleichzeitige I/O-Aufgaben

Typische Beispiele:

  • Webserver, die viele gleichzeitige HTTP-Anfragen beantworten.
  • Web-Scraper oder Clients, die viele HTTP-Anfragen an andere Server stellen.
  • Chat-Server, WebSockets, Streaming.
  • Programme, die gleichzeitig:
    • Dateien lesen/schreiben,
    • mit einer Datenbank kommunizieren,
    • HTTP-Anfragen senden.

Synchroner Ansatz:
Jede Anfrage blockiert einen Thread/Prozess, solange sie auf Antwort wartet → sehr viele Threads/Prozesse nötig.

Asynchroner Ansatz:
Ein Event Loop verwaltet viele tausend Verbindungen in einem oder wenigen Threads, indem er immer dort weiterarbeitet, wo gerade Daten verfügbar sind.

5.2 Bessere Ressourcennutzung bei I/O-lastigen Programmen

Wenn dein Programm hauptsächlich:

  • Daten lädt (HTTP, DB),
  • auf Antworten wartet,
  • nicht viel rechnet,

dann ist asynchroner Code oft effizienter (weniger Overhead, weniger Threads, bessere Skalierung).

5.3 Responsivere Anwendungen (z.B. GUIs)

In grafischen Anwendungen (oder auch CLI-Tools) willst du:

  • Nicht, dass die Oberfläche „einfriert“, während eine Anfrage ans Internet läuft.
  • Stattdessen nutzt du asynchrone oder nebenläufige Mechanismen, damit der Haupt-Thread weiterhin Eingaben entgegennimmt.

6. Praxisnahe Beispiele

6.1 Vergleich: synchron vs. asynchron HTTP-Anfragen

Synchron: nacheinander mit requests

import requests

urls = [
    "https://example.com",
    "https://httpbin.org/delay/2",
    "https://httpbin.org/delay/3",
]

def fetch(url):
    print(f"Rufe {url} ab...")
    response = requests.get(url)
    print(f"{url}: Status {response.status_code}")

def main():
    for url in urls:
        fetch(url)

if __name__ == "__main__":
    main()
  • Jede Anfrage wartet, bis sie fertig ist.
  • Gesamtzeit ≈ Summe aller Wartezeiten.

Asynchron: gleichzeitig mit aiohttp und asyncio

import asyncio
import aiohttp

urls = [
    "https://example.com",
    "https://httpbin.org/delay/2",
    "https://httpbin.org/delay/3",
]

async def fetch(session, url):
    print(f"Rufe {url} ab...")
    async with session.get(url) as response:
        print(f"{url}: Status {response.status}")

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        await asyncio.gather(*tasks)  # starte alle gleichzeitig

if __name__ == "__main__":
    asyncio.run(main())
  • Alle Anfragen werden „gleichzeitig“ gestartet.
  • Gesamtzeit ≈ maximale Einzeldauer, nicht Summe.

6.2 Viele „Schlaf-Aufgaben“ parallel (Simulation von I/O)

import asyncio
import random

async def simulierte_io_aufgabe(n):
    dauer = random.uniform(0.5, 2.0)
    print(f"Aufgabe {n} startet, Dauer ~{dauer:.2f}s")
    await asyncio.sleep(dauer)
    print(f"Aufgabe {n} fertig")

async def main():
    tasks = [simulierte_io_aufgabe(i) for i in range(5)]
    await asyncio.gather(*tasks)

asyncio.run(main())

Output (ähnlich):

  • Mehrere Aufgaben starten schnell hintereinander.
  • Sie enden in anderer Reihenfolge, je nach Dauer.
  • Die Gesamtzeit liegt in etwa bei der längsten Wartezeit, nicht bei der Summe aller.

7. Typische Herausforderungen und Stolperfallen

7.1 Denken in „async“ ist ungewohnt

Für Einsteiger:

  • Man kann await nur in async def-Funktionen verwenden.
  • Asynchrone Funktionen verhalten sich anders als normale:
async def foo():
    return 42

# Aufruf:
result = foo()  # das ist KEINE 42, sondern eine Coroutine!

Du musst sie ausführen:

import asyncio

async def foo():
    return 42

async def main():
    result = await foo()
    print(result)

asyncio.run(main())

7.2 Blockierender Code in asynchronem Kontext

Problem:

  • Du hast eine async-Funktion, benutzt darin aber eine blockierende Bibliothek (z.B. requests, time.sleep).
  • Dann blockierst du trotzdem den Event Loop, obwohl du „async“ verwendest.

Beispiel (so besser nicht):

import asyncio
import time

async def schlecht():
    print("Blockiere Event Loop...")
    time.sleep(5)  # blockiert den Event Loop komplett!
    print("Weiter geht's")

asyncio.run(schlecht())

Lösung:

  • Entweder eine asynchrone Alternative verwenden (z.B. aiohttp statt requests).
  • Oder blockierende Funktion in einem Thread/Prozess auslagern (z.B. asyncio.to_thread).
import asyncio
import time

def blockierende_funktion():
    time.sleep(5)
    return "fertig"

async def gut():
    print("Starte blockierende Funktion in Thread...")
    result = await asyncio.to_thread(blockierende_funktion)
    print("Ergebnis:", result)

asyncio.run(gut())

7.3 Debugging und Fehlersuche

  • Fehler in asynchronen Programmen können schwerer nachzuvollziehen sein.
  • Stack-Traces sehen anders aus, weil Coroutines, Tasks und der Event Loop beteiligt sind.
  • Es kann passieren, dass Tasks „stillschweigend“ fehlschlagen, wenn man sie nicht korrekt awaited oder Fehler nicht abfängt.

7.4 Testen von asynchronem Code

  • Unit-Tests brauchen meist auch einen Event Loop.
  • Viele Testframeworks bieten dafür Mechanismen (pytest mit pytest-asyncio).

Beispiel mit pytest-asyncio:

# test_example.py
import pytest
import asyncio

async def verdoppeln(x):
    await asyncio.sleep(0.1)
    return x * 2

@pytest.mark.asyncio
async def test_verdoppeln():
    assert await verdoppeln(21) == 42

8. Wann lohnt sich asynchrones Programmieren (und wann nicht)?

Sinnvoll bei:

  • Web-APIs, Microservices, Webserver.
  • Chat-Server, WebSocket-Anwendungen.
  • Web-Scraping vieler Seiten gleichzeitig.
  • I/O-lastigen Programmen mit vielen Netzwerk- oder Datenbankzugriffen.

Weniger sinnvoll bei:

  • Reinen CPU-lastigen Aufgaben (z.B. numerische Berechnungen, Bildverarbeitung).
    • Hier helfen eher: mehrere Prozesse (multiprocessing) oder spezialisierte Bibliotheken (NumPy, Numba, etc.).
  • Kleinen Scripts, die nur wenige, einfache Schritte nacheinander machen da ist synchroner Code oft einfacher und ausreichend.

9. Zusammenfassung in einfachen Worten

  • Asynchron in Python bedeutet:
    Du kannst zeitaufwändige, I/O-lastige Aufgaben starten und in der Zwischenzeit andere Aufgaben erledigen, statt auf jede einzelne zu warten.

  • Die Mechanismen dafür sind:

    • async def (Coroutines),
    • await (warten, ohne zu blockieren),
    • ein Event Loop (z.B. in asyncio).
  • Es löst besonders gut Probleme mit vielen gleichzeitigen I/O-Operationen (Netzwerk, Datenbanken), wie bei Webservern und Web-Scrapern.

  • Herausforderungen:

    • Umdenken gegenüber normalem, synchronem Code.
    • Aufpassen, keine blockierenden Funktionen im Event Loop zu verwenden.
    • Debugging und Testen sind etwas komplexer.