Files
work/notes/nextcloud/Notes/IT-Know-How/python/Asynchron in Python.md
T

412 lines
12 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.