EMZETT.
Login

Kurz: 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.

Teil des Kurses Python

Kapitel 43 von 44 im Kurs Python. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

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.

WerkzeugGut fürPrinzip
threadingWarten (Netzwerk, Dateien)mehrere Abläufe im selben Prozess
multiprocessingRechnen auf mehreren Kernenmehrere Prozesse
asynciosehr viele wartende Aufgabenein Ablauf, der bei Wartezeiten umschaltet

Hinweis

In CPython sorgt der GIL (Global Interpreter Lock) dafür, dass immer nur ein Thread Python-Code ausführt. Threads beschleunigen daher Warten, aber keine reine Rechenarbeit. Dafür nimmst du Prozesse.

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)

Ausgabe:

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)

Ausgabe:

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)

Ausgabe:

[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])))

Ausgabe:

[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())

Ausgabe:

['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())

Ausgabe:

zu langsam, abgebrochen

Was wann?

  • Viele Netzwerkanfragen oder Datenbankabfragen: asyncio oder Threads
  • Rechenintensive Schleifen: multiprocessing
  • Einfache Hintergrundaufgabe: ein Thread
  • Bibliotheken wie aiohttp und httpx bringen asynchrone Webzugriffe

Merke

  • Threads: gleichzeitiges Warten; der GIL bremst reine Rechenarbeit
  • Lock schützt gemeinsame Daten vor Wettlaufsituationen
  • concurrent.futures bietet Pools für Threads und Prozesse
  • Prozesse nutzen mehrere Kerne
  • async def und await bilden Coroutinen; asyncio.run startet sie, gather bündelt

Übungsaufgabe

Schreibe ein Programm, das mit asyncio.gather drei “Downloads” (simuliert mit asyncio.sleep) gleichzeitig ausführt und die Gesamtdauer misst.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Kommandozeilen-Programme mit argparse

Weiter: Guter Stil: PEP 8 und Pythonic

Alle Kapitel: Python im Überblick