12 KiB
Executable File
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
asyncionur als externes Paket. - Ab Python 3.5 wurden die Schlüsselwörter
asyncundawaiteingefü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
awaitnur inasync 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.
aiohttpstattrequests). - 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 (
pytestmitpytest-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.).
- Hier helfen eher: mehrere Prozesse (
- 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.