Manchmal soll ein Programm mehrere Dinge gleichzeitig tun: auf mehrere Webseiten warten, Dateien parallel verarbeiten oder eine Oberfläche bedienbar halten. Python bietet dafür drei Wege.
| Werkzeug | Gut für | Prinzip |
|---|---|---|
threading | Warten (Netzwerk, Dateien) | mehrere Abläufe im selben Prozess |
multiprocessing | Rechnen auf mehreren Kernen | mehrere Prozesse |
asyncio | sehr viele wartende Aufgaben | ein Ablauf, der bei Wartezeiten umschaltet |
Threads
import threading, time
def arbeiter(name, dauer):
time.sleep(dauer)
print(f"{name} fertig")
start = time.perf_counter()
t1 = threading.Thread(target=arbeiter, args=("A", 0.3))
t2 = threading.Thread(target=arbeiter, args=("B", 0.1))
t1.start(); t2.start()
t1.join(); t2.join() # warten, bis beide fertig sind
print("zusammen unter 0.5 s:", time.perf_counter() - start < 0.5)B fertig A fertig zusammen unter 0.5 s: True
Beide Wartezeiten laufen gleichzeitig; die Gesamtzeit ist etwa so lang wie die längere.
Gemeinsame Daten schützen
Wenn mehrere Threads dieselbe Variable ändern, kann es zu Wettlaufsituationen kommen. Ein Lock sorgt dafür, dass nur einer gleichzeitig ändert:
import threading
zaehler = 0
lock = threading.Lock()
def erhoehe():
global zaehler
for _ in range(10000):
with lock:
zaehler += 1
threads = [threading.Thread(target=erhoehe) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(zaehler)40000
ThreadPoolExecutor
Bequemer geht es mit einem Pool aus concurrent.futures:
from concurrent.futures import ThreadPoolExecutor
import time
def hole(nr):
time.sleep(0.1) # tut so, als würde eine Seite geladen
return nr * nr
with ThreadPoolExecutor(max_workers=5) as pool:
ergebnisse = list(pool.map(hole, range(1, 6)))
print(ergebnisse)[1, 4, 9, 16, 25]
Prozesse für Rechenarbeit
Mit ProcessPoolExecutor verteilst du Rechnungen auf mehrere Kerne:
from concurrent.futures import ProcessPoolExecutor
def quadratsumme(n):
return sum(i * i for i in range(n))
if __name__ == "__main__":
with ProcessPoolExecutor() as pool:
print(list(pool.map(quadratsumme, [1000, 2000, 3000])))[332833500, 2664667000, 8995500500]
Der Schutz if __name__ == "__main__": ist hier Pflicht, damit Kindprozesse das Skript nicht erneut starten.
asyncio
Bei asyncio läuft alles in einem einzigen Ablauf. Eine Funktion mit async def ist eine Coroutine; mit await gibt sie die Kontrolle ab, während sie wartet. So kann der Ablauf in der Zwischenzeit andere Aufgaben bedienen:
import asyncio
async def hole(name, dauer):
await asyncio.sleep(dauer)
return f"{name} nach {dauer} s"
async def main():
ergebnisse = await asyncio.gather(hole("A", 0.3), hole("B", 0.1), hole("C", 0.2))
print(ergebnisse)
asyncio.run(main())['A nach 0.3 s', 'B nach 0.1 s', 'C nach 0.2 s']
gather startet alle Aufgaben gleichzeitig und liefert die Ergebnisse in der übergebenen Reihenfolge.
Zeitüberschreitung und Abbruch
import asyncio
async def langsam():
await asyncio.sleep(5)
async def main():
try:
await asyncio.wait_for(langsam(), timeout=0.1)
except asyncio.TimeoutError:
print("zu langsam, abgebrochen")
asyncio.run(main())zu langsam, abgebrochen
Was wann?
- Viele Netzwerkanfragen oder Datenbankabfragen:
asynciooder Threads - Rechenintensive Schleifen:
multiprocessing - Einfache Hintergrundaufgabe: ein Thread
- Bibliotheken wie
aiohttpundhttpxbringen asynchrone Webzugriffe
Merke
- Threads: gleichzeitiges Warten; der GIL bremst reine Rechenarbeit
Lockschützt gemeinsame Daten vor Wettlaufsituationenconcurrent.futuresbietet Pools für Threads und Prozesse- Prozesse nutzen mehrere Kerne
async defundawaitbilden Coroutinen;asyncio.runstartet sie,gatherbündelt
Aufgabe
Schreibe ein Programm, das mit asyncio.gather drei "Downloads" (simuliert mit asyncio.sleep) gleichzeitig ausführt und die Gesamtdauer misst.