412 lines
12 KiB
Markdown
Executable File
412 lines
12 KiB
Markdown
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):
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
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
|
||
|
||
```python
|
||
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`
|
||
|
||
```python
|
||
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`
|
||
|
||
```python
|
||
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)
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
async def foo():
|
||
return 42
|
||
|
||
# Aufruf:
|
||
result = foo() # das ist KEINE 42, sondern eine Coroutine!
|
||
```
|
||
|
||
Du musst sie ausführen:
|
||
|
||
```python
|
||
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):
|
||
|
||
```python
|
||
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`).
|
||
|
||
```python
|
||
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`:
|
||
|
||
```python
|
||
# 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.
|
||
|