Ihre Programme haben bisher immer nur eine Sache auf einmal erledigt, und für elf Lektionen war das auch richtig. Aber stellen Sie sich ein Skript vor, das hundert Webseiten herunterlädt: Jede Anfrage verbringt zwei Sekunden mit Warten auf einen Server und fast nichts mit Rechnen. Nacheinander ausgeführt bedeutet das mehr als drei Minuten, in denen Ihr Programm Däumchen dreht. Nebenläufigkeit ist die Familie von Techniken, um diese Wartezeiten zu überlappen, und dieselben hundert Seiten sind in wenigen Sekunden fertig. Diese Lektion ist die ungeschminkte, ehrliche Einführung: was Nebenläufigkeit ist und was nicht, die drei Werkzeuge, die Python bietet, und wann genau Sie zu welchem greifen.
Nebenläufigkeit hat den Ruf, schwierig zu sein, und dieser Ruf ist im Allgemeinen verdient, aber für Sie vermeidbar, denn die Schwierigkeit steckt fast ausschließlich im gemeinsamen veränderlichen Zustand: Threads, die gleichzeitig auf dieselben Daten schreiben. Die modernen Muster, die diese Lektion vermittelt, Executor-Maps und Async-Tasks, sind so konzipiert, dass Sie dieses heiße Eisen kaum anfassen müssen. Das Ziel heute ist nicht die Beherrschung jeder Primitiven, sondern ein korrektes mentales Modell und zwei funktionierende Rezepte, womit Sie einem erstaunlichen Teil der arbeitenden Programmierer voraus sind.
Was Sie in Teil 12 lernen werden
- I/O-gebunden versus CPU-gebunden: die eine Diagnose, die alles entscheidet
- Threads mit ThreadPoolExecutor: Wartezeiten in vier Zeilen überlappen
- Der GIL, ehrlich und ohne Panik erklärt
- Prozesse für wirklich parallele Berechnungen
- asyncio: async, await und gather für tausende gleichzeitige Wartevorgänge
- Wie Sie sich jedes Mal zwischen Threads, Prozessen und Async entscheiden
Hinweis
Bevor Sie beginnen
Sie benötigen Funktionen aus Teil 4, Ausnahmen aus Teil 7 und das Generator-Verständnis aus Teil 9, denn Async-Funktionen pausieren und setzen genau so fort, wie Generatoren es Ihnen beigebracht haben. Dies ist die konzeptionellste Lektion des Kurses; lesen Sie langsam und führen Sie alles aus.
1. Die Diagnose: Warten Sie oder rechnen Sie?
Jedes Performance-Problem in diesem Bereich beginnt mit einer Frage. Ein I/O-gebundenes Programm verbringt seine Zeit mit Warten auf Dinge außerhalb der CPU: Netzwerkantworten, Festplattenzugriffe, Datenbankantworten, einen Benutzer. Ein CPU-gebundenes Programm verbringt seine Zeit mit Rechnen: Bildverarbeitung, Kryptografie, Modelltraining, Zahlenverarbeitung. Die Unterscheidung bestimmt fast ausnahmslos Ihr Werkzeug: I/O-gebundene Arbeit verlangt nach Threads oder asyncio, die die Wartezeiten verzahnen; CPU-gebundene Arbeit verlangt nach Prozessen, die weitere Prozessorkerne hinzuziehen. Eine Fehldiagnose führt dazu, dass Ihre Nebenläufigkeit nichts schneller macht, aber sämtliche Kosten verursacht.
| Ihr Engpass | Werkzeug | Warum es funktioniert |
|---|---|---|
| Warten auf viele Netzwerkaufrufe oder Dateien | Threads oder asyncio | Während eine Aufgabe wartet, läuft eine andere; die Wartezeiten überlappen sich |
| Schwere Berechnungen auf mehreren Kernen | multiprocessing | Separate Prozesse umgehen den GIL und nutzen alle Kerne |
| Tausende gleichzeitige Verbindungen | asyncio | Tasks sind bei großem Maßstab weitaus günstiger als Threads |
| Eine Handvoll blockierender Aufrufe, die beschleunigt werden sollen | ThreadPoolExecutor | Kleinste Änderung an bestehendem Code, vier Zeilen |
| Einfaches Skript, keine echten Wartezeiten | keine | Nebenläufigkeit erhöht die Komplexität; verdienen Sie sie sich mit einem echten Engpass |
2. Threads: Wartezeiten überlappen
Ein Thread ist ein unabhängiger Ausführungsstrang innerhalb Ihres Programms; das Betriebssystem schaltet zwischen Threads um, und wenn einer bei einem Netzwerkzugriff blockiert, machen andere weiter. Rohes Thread-Management ist fummelig, daher verpackt modernes Python es in einen Executor: Erstellen Sie einen Pool, mappen Sie Ihre Funktion über Ihre Eingaben, sammeln Sie die Ergebnisse in der richtigen Reihenfolge. Diese vier Zeilen sind das mit Abstand nützlichste Nebenläufigkeitsrezept der Sprache und verwandeln den Drei-Minuten-Downloader in einen Sechs-Sekunden-Downloader, ohne etwas umzustrukturieren.
from concurrent.futures import ThreadPoolExecutor
import time
def fetch(url): # stand-in for a real network call
time.sleep(1) # the waiting we want to overlap
return f"{url}: ok"
urls = [f"https://site/{n}" for n in range(8)]
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(fetch, urls))
elapsed = time.perf_counter() - start
print(results[0], "...")
print(f"8 one-second fetches in {elapsed:.1f}s") # ~1.0s, not 8
Nun das berühmte Sternchen: der Global Interpreter Lock. CPython, der Standard-Interpreter, erlaubt zu jedem Zeitpunkt nur einem Thread, Python-Bytecode auszuführen. Für I/O-gebundene Arbeit spielt das kaum eine Rolle, denn wartende Threads geben den Lock frei, weshalb der obige Downloader tatsächlich schneller wird. Für CPU-gebundene Arbeit spielt es eine entscheidende Rolle: Acht Threads, die Zahlen verarbeiten, wechseln sich auf einem Kern ab und werden nicht schneller fertig, manchmal sogar langsamer. Der GIL ist kein Skandal, sondern ein Design-Kompromiss, und neuere Python-Versionen bieten allmählich einen GIL-freien Build an; aber die Leitlinie für Sie bleibt unverändert: Threads zum Warten, Prozesse zum Rechnen.
Prozesse sind das schwere Werkzeug: separate Python-Interpreter mit getrenntem Speicher, koordiniert durch das multiprocessing-Modul oder, angenehmer, durch dieselbe Executor-API mit ausgetauschtem ProcessPoolExecutor. Jeder Prozess hat seinen eigenen GIL, sodass acht Prozesse tatsächlich acht Kerne nutzen. Die Kosten sind real: langsamerer Start und Daten, die zwischen Prozessen kopiert statt geteilt werden, weshalb Sie sie für echte Berechnungen reservieren. Wenn Teil 15 neuronale Netze trainiert, leisten die darunterliegenden Bibliotheken genau diese Art von Parallelität in optimiertem C, was auch die tiefere Lektion ist: In der Datenarbeit nutzen Sie meist die Parallelität, die Bibliotheken für Sie bereitgestellt haben.
Eine praktische Frage, die sich jedem Executor-Nutzer sofort stellt: Wie viele Worker? Bei I/O-lastigen Thread-Pools richtet sich die Antwort nach der Wartezeit, nicht nach der Kernzahl; acht, sechzehn oder mehr Threads sind sinnvoll, wenn jeder sein Leben mit blockierenden Serveraufrufen verbringt: die höfliche Obergrenze ist das, was der entfernte Dienst verkraftet. Bei Prozess-Pools bringen mehr Worker als CPU-Kerne nichts und kosten nur Speicher, daher ist os.cpu_count() die natürliche Vorgabe. In beiden Fällen ist die ehrliche Methode die, die dieser Kurs durchgängig lehrt: eine vernünftige Zahl wählen, mit perf_counter messen, einmal anpassen und dann nicht weiter optimieren.
Kontrollfrage
Ihr Skript verkleinert 10.000 Fotos und lastet einen CPU-Kern voll aus. Welches Werkzeug hilft tatsächlich?
3. asyncio: Nebenläufigkeit als Sprachmerkmal
Das dritte Werkzeug verlagert Nebenläufigkeit in die Sprache selbst. Eine async-def-Funktion ist eine Coroutine: Ihr Aufruf erzeugt eine wartende Aufgabe, statt den Rumpf auszuführen: genau das verzögerte Verhalten, das Sie von Generatoren aus Teil 9 kennen, und das ist kein Zufall, Coroutinen sind direkt aus der Generator-Mechanik hervorgegangen. Innerhalb einer Coroutine markiert await die Stellen, an denen die Funktion pausieren kann; während sie pausiert, führt die Ereignisschleife, ein in asyncio eingebauter Scheduler, andere Coroutinen aus. Ein Thread, Tausende Tasks, Umschaltung nur an await-Punkten: Das ist das gesamte Modell.
import asyncio
async def fetch(url: str) -> str:
await asyncio.sleep(1) # a polite, non-blocking wait
return f"{url}: ok"
async def main() -> None:
urls = [f"https://site/{n}" for n in range(100)]
results = await asyncio.gather(*(fetch(u) for u in urls))
print(f"{len(results)} fetches done")
asyncio.run(main()) # ~1 second for all 100
Lesen Sie main sorgfältig, denn es enthält die beiden Idiome, die asynchrones Python tragen. gather nimmt viele Coroutinen entgegen, führt sie nebenläufig aus und liefert alle Ergebnisse in Reihenfolge zurück; der Generatorausdruck, der es speist, ist wieder Ihre Fertigkeit aus Teil 9. Und asyncio.run ist die Brücke aus der gewöhnlichen Welt: Es startet die Ereignisschleife, führt eine Coroutine bis zum Ende aus und räumt auf. Die Regeln, die aus dem Modell folgen: await nur innerhalb von async def, niemals blockierende Funktionen wie time.sleep in einer Coroutine aufrufen, die asynchronen Entsprechungen verwenden und bedenken, dass async nur hilft, wenn es Wartezeiten zu überlappen gibt; für reine Rechenarbeit bringt es nichts.
Wann sollte ein Anfänger tatsächlich asyncio gegenüber Threads wählen? Maßstab und Ökosystem. Bei einem Dutzend gleichzeitiger Wartevorgänge ist der Thread-Pool einfacher und Ihre Bibliotheken müssen nicht kooperieren. Bei Hunderten oder Tausenden, oder wenn Sie mit Frameworks arbeiten, die async-nativ sind, gewinnt asyncio eindeutig: Tasks kosten fast nichts, und die await-Punkte machen die Umschaltung im Code sichtbar. Moderne Web-Backends leben dort, weshalb unser Fortgeschrittenen-Track dem eine ganze Lektion widmet; der async-Python-Tiefgang mit httpx setzt genau dort an, wo wir heute enden, mit echten Netzwerkaufrufen und Produktionsmustern.
Kontrollfrage
Was bewirkt await tatsächlich innerhalb einer async-Funktion?
4. Praxis: ein Simulator für gleichzeitige Downloads
Der folgende Spielplatz führt echtes asyncio in Ihrem Browser aus und simuliert einen Batch-Downloader mit variablen Verzögerungen, einer Nebenläufigkeitsbegrenzung und Fehlerbehandlung pro Task: die ehrliche Gestalt von produktivem Async-Code. Ein browserspezifischer Hinweis, der selbst eine gute Lektion ist: Diese Seite läuft bereits innerhalb einer Ereignisschleife, daher verwenden wir anstelle von asyncio.run(main()) direkt await main(), genau wie Sie es in einem Jupyter-Notebook tun würden, während auf Ihrem eigenen Rechner die asyncio.run-Form aus Abschnitt 3 korrekt ist.
In dieser vereinfachten Implementierung verbergen sich zwei produktionsreife Details. Das Semaphor begrenzt gleichzeitige Arbeit, wie es echte Server von dir verlangen; unbegrenzte Parallelität ist ein Denial-of-Service-Angriff mit zusätzlichen Schritten. Und return_exceptions=True macht gather zu einem Sammler von Ergebnissen statt zu einem einzelnen Fehlerpunkt: die Philosophie aus Teil 7, Fehler als zu behandelnde Werte, skaliert auf hundert Tasks. Behalte beide Gewohnheiten bei, und dein erstes echtes Async-Programm wird bereits wie von einem Senior aussehen.
! Häufige Fehler, die du vermeiden solltest
-
✕Threads zu einer CPU-gebundenen Schleife hinzufügen und null Beschleunigung sehen.
✓Der GIL serialisiert Python-Berechnungen über Threads hinweg. Stelle zuerst die Diagnose: Warten will Threads oder Async, Rechnen will ProcessPoolExecutor.
-
✕time.sleep oder andere blockierende Funktionen innerhalb von Async-Code aufrufen.
✓Ein blockierender Aufruf friert die gesamte Ereignisschleife und jeden Task darauf ein. Verwende innerhalb von async def await asyncio.sleep und asynchrone Bibliotheken; ein einziger blockierender Aufruf macht alles zunichte.
-
✕Eine asynchrone Funktion wie eine normale aufrufen und sich wundern, warum nichts passiert ist.
✓async def gibt ein Coroutine-Objekt zurück; nichts läuft, bis es mit await versehen oder an asyncio.run/gather übergeben wird. Die Warnung „coroutine was never awaited“ bedeutet genau das.
-
✕Datenstrukturen beiläufig über Threads hinweg teilen und verändern.
✓Dort wohnen die Gruselgeschichten der Nebenläufigkeit. Bevorzuge die gezeigten Muster: Eingaben auf Ausgaben abbilden, Ergebnisse einsammeln, nichts Geteiltes verändern. Wenn du teilen musst, brauchst du Sperren, und das ist ein Zeichen zum Umgestalten.
? Häufig gestellte Fragen
Wird der GIL verschwinden? +
Python liefert jetzt einen optionalen Build ohne GIL (free-threaded), der seit 3.13 stetig reift. Das Ökosystem migriert schrittweise. Die Diagnosedisziplin, die du heute gelernt hast, bleibt so oder so richtig, und deshalb lehrt diese Lektion sie zuerst.
Brauche ich Nebenläufigkeit für meine Skripte? +
Meistens nicht, und das ist eine gute Antwort. Greife danach, wenn ein echter Engpass auftritt: viele langsame Netzwerkaufrufe sind der Klassiker. Nebenläufigkeit ist ein Werkzeug für messbare Probleme, keine Standardhaltung.
Async oder Threads für mein erstes nebenläufiges Programm? +
ThreadPoolExecutor.map für eine schnelle Beschleunigung von bestehendem blockierendem Code; asyncio, wenn du ein neues Programm rund um Netzwerkaufrufe beginnst oder wenn die Anzahl der gleichzeitigen Vorgänge in die Dutzende geht. Viele funktionierende Programme nutzen beides: Async im Kern, einen Thread-Pool für hartnäckige blockierende Bibliotheken.
Wie hängt das mit den Webframeworks zusammen, von denen ich ständig höre? +
Direkt: Moderne Python-Webserver sind asyncio-Ereignisschleifen, und jeder Request-Handler ist eine Coroutine. Wenn du nach diesem Track mit unserer FastAPI-Serie weitermachst, ist das heutige mentale Modell das Fundament, auf dem alles aufbaut.
5. Zusammenfassung und Ausblick
Du kannst jetzt I/O-gebunden von CPU-gebunden unterscheiden und die Diagnose das Werkzeug wählen lassen: ThreadPoolExecutor, um eine Handvoll Wartezeiten zu überlappen, ProcessPoolExecutor für echte Mehrkernberechnung, asyncio mit gather, Semaphoren und Ausnahmesammlung für Nebenläufigkeit im großen Maßstab. Du verstehst den GIL als Kompromiss statt als Rätsel, und du kennst die Kardinalsünden: die Schleife blockieren und veränderlichen Zustand teilen: gut genug, um sie von vornherein zu vermeiden.
Als Nächstes kommt die Lektion, die deinen Stand als Programmierer leise verändert: Teil 13, Testen mit pytest, wo du lernst zu beweisen, dass dein Code funktioniert, und ihn funktionsfähig hältst, während du ihn änderst. Die Concurrency-Lektion in der Learn-Python-App unten wiederholt die heutige Entscheidungstabelle in Quizform, und das vollständige Curriculum findest du auf der Serienübersicht.
Profitipp
Bevor du irgendeine Nebenläufigkeit hinzufügst, miss: time.perf_counter() um den langsamen Teil herum, und finde heraus, ob du wartest oder rechnest. Zehn Zeilen Messung sparen routinemäßig hundert Zeilen fehlplatzierter Threads, und Profiling vor Optimierung ist die erwachsenste Angewohnheit in der Programmierung.
Üben für unterwegs
Learn Python, die kostenlose Android-App
Jedes Thema dieser Reihe gibt es auch in der App: mundgerechte Lektionen, ausführbare Beispiele, Quizze, Miniprojekte und eine offline nutzbare Python-Umgebung, die auf Ihrem Handy läuft.
Kommentare
0Noch keine Kommentare. Teile als Erste oder Erster deine Gedanken.