Das `concurrent`-Modul in Python
In diesem Artikel erklären wir das concurrent-Modul in Python.
Wir erläutern die Konzepte von Nebenläufigkeit und Parallelität und erklären anhand praktischer Beispiele, wie man mit dem concurrent-Modul asynchrone Verarbeitung umsetzt.
YouTube Video
Das concurrent-Modul in Python
Wenn man die Verarbeitung in Python beschleunigen möchte, ist es wichtig, die Unterschiede zwischen Nebenläufigkeit und Parallelität zu beachten. Das concurrent-Modul ist ein wichtiger Weg, um asynchrone Verarbeitung sicher und einfach unter Berücksichtigung dieser Unterschiede zu handhaben.
Der Unterschied zwischen Nebenläufigkeit und Parallelität
-
Nebenläufigkeit bedeutet, einen Ablauf so zu gestalten, dass mehrere Aufgaben durch Wechsel zwischen ihnen in kleinen Arbeitseinheiten voranschreiten. Auch wenn Aufgaben nicht tatsächlich gleichzeitig laufen, kann die Nutzung von 'Wartezeiten' dazu beitragen, den gesamten Prozess effizienter zu gestalten.
-
Parallelität ist ein Mechanismus, der mehrere Aufgaben physisch gleichzeitig ausführt. Durch die Nutzung mehrerer CPU-Kerne wird die Verarbeitung gleichzeitig vorangetrieben.
Beides sind Techniken zur Beschleunigung der Verarbeitung, aber Nebenläufigkeit ist eine Gestaltungsfrage des 'Wie geht es weiter?', während Parallelität eine Ausführungsfrage des 'Wie läuft es ab?' ist, was sie grundlegend unterschiedlich macht.
Was ist das concurrent-Modul?
concurrent ist eine Standardbibliothek in Python, die eine High-Level-API bietet, um Nebenläufigkeit und Parallelität sicher und einfach zu handhaben. Sie ist so konzipiert, dass man sich auf das 'Ausführen von Aufgaben' konzentrieren kann, ohne sich um Low-Level-Operationen wie das Erstellen und Verwalten von Threads oder Prozessen kümmern zu müssen.
Rollen von ThreadPoolExecutor und ProcessPoolExecutor
Das concurrent-Modul bietet zwei Hauptoptionen, je nach Art der Aufgabe.
-
ThreadPoolExecutorDies eignet sich für nebenläufige Implementierungen, insbesondere bei Aufgaben mit vielen I/O-Wartezeiten wie Netzwerk- oder Dateioperationen. Durch den Wechsel zwischen Aufgaben wird die Wartezeit effektiv genutzt. -
ProcessPoolExecutorDiese Implementierung ist auf parallele Verarbeitung ausgerichtet und eignet sich für CPU-intensive Aufgaben. Es werden mehrere Prozesse genutzt, um die verfügbaren CPU-Kerne parallel voll auszuschöpfen.
Eine Hauptfunktion des concurrent-Moduls ist daher, dass es eine Struktur bereitstellt, mit der man je nach Bedarf gezielt zwischen Nebenläufigkeit und Parallelität wählen kann.
Grundlagen von ThreadPoolExecutor (für I/O-Aufgaben)
ThreadPoolExecutor ist geeignet für I/O-lastige Aufgaben, wie z. B. Netzwerkkommunikation und Dateioperationen. Es verteilt Aufgaben auf mehrere Threads und nutzt die Wartezeit effizient aus.
1from concurrent.futures import ThreadPoolExecutor
2import time
3
4def fetch_data(n):
5 # Simulate an I/O-bound task
6 time.sleep(1)
7 return f"data-{n}"
8
9with ThreadPoolExecutor(max_workers=3) as executor:
10 futures = [executor.submit(fetch_data, i) for i in range(5)]
11
12 for future in futures:
13 print(future.result())- In diesem Beispiel werden mehrere I/O-Aufgaben, die jeweils eine Sekunde warten, nebenläufig ausgeführt. Mit
submitwerden Funktionsaufrufe als asynchrone Aufgaben registriert, und durch Aufruf vonresult()kann man auf deren Abschluss warten und die Ergebnisse erhalten, was eine knappe Implementierung von nebenläufiger Verarbeitung und effektiver Nutzung von Wartezeiten ermöglicht.
Einfache Nebenläufigkeit mit map
Wenn keine komplexe Steuerung nötig ist, kann die Verwendung von map den Code noch kompakter machen.
1from concurrent.futures import ThreadPoolExecutor
2import time
3
4def fetch_data(n):
5 # Simulate an I/O-bound task
6 time.sleep(1)
7 return f"data-{n}"
8
9with ThreadPoolExecutor(max_workers=3) as executor:
10 results = executor.map(fetch_data, range(5))
11
12 for result in results:
13 print(result)- In diesem Beispiel werden mehrere I/O-Aufgaben mit
ThreadPoolExecutor.mapnebenläufig ausgeführt. Damapdie Ergebnisse in derselben Reihenfolge wie die Eingaben zurückgibt, kann der Code ähnlich wie in der sequentiellen Verarbeitung geschrieben werden, und nebenläufige Ausführung ist möglich, ohne dass man sich um asynchrone Verarbeitung kümmern muss – das ist ein großer Vorteil.
Grundlagen von ProcessPoolExecutor (für CPU-lastige Aufgaben)
Für rechenintensive Aufgaben, die die CPU voll ausnutzen, sollten Prozesse statt Threads verwendet werden. Dadurch kann die Einschränkung durch das Global Interpreter Lock (GIL) umgangen werden.
1from concurrent.futures import ProcessPoolExecutor
2
3def heavy_calculation(n):
4 # Simulate a CPU-bound task
5 total = 0
6 for i in range(10_000_000):
7 total += i * n
8 return total
9
10if __name__ == "__main__":
11 with ProcessPoolExecutor(max_workers=4) as executor:
12 results = executor.map(heavy_calculation, range(4))
13
14 for result in results:
15 print(result)In diesem Beispiel werden CPU-intensive Berechnungen parallel mit dem ProcessPoolExecutor ausgeführt. Da Prozesse erstellt werden, ist eine __main__-Abfrage erforderlich, die eine sichere parallele Verarbeitung unter Nutzung mehrerer CPU-Kerne ermöglicht.
Verarbeitung nach Abschlussreihenfolge mit as_completed
as_completed ist praktisch, wenn man die Ergebnisse in der Reihenfolge ihres Abschlusses verarbeiten möchte.
1from concurrent.futures import ThreadPoolExecutor, as_completed
2import time
3
4def fetch_data(n):
5 # Simulate an I/O-bound task
6 time.sleep(1)
7 return f"data-{n}"
8
9with ThreadPoolExecutor(max_workers=3) as executor:
10 futures = [executor.submit(fetch_data, i) for i in range(5)]
11
12 for future in as_completed(futures):
13 print(future.result())- In diesem Beispiel werden mehrere asynchrone Aufgaben gleichzeitig ausgeführt und die Ergebnisse in der Reihenfolge ihres Abschlusses abgeholt. Mit
as_completedkönnen Ergebnisse unabhängig von der Aufgabenreihenfolge schnell verarbeitet werden, was es z. B. für Fortschrittsanzeigen oder Situationen mit sequentieller Verarbeitung nützlich macht.
Umgang mit Ausnahmen
Im concurrent-Modul werden Ausnahmen beim Aufruf von result() ausgelöst.
1from concurrent.futures import ThreadPoolExecutor
2
3def risky_task(n):
4 # Simulate a task that may fail for a specific input
5 if n == 3:
6 raise ValueError("Something went wrong")
7 return n * 2
8
9with ThreadPoolExecutor() as executor:
10 futures = [executor.submit(risky_task, i) for i in range(5)]
11
12 for future in futures:
13 try:
14 print(future.result())
15 except Exception as e:
16 print("Error:", e)- Dieses Beispiel zeigt, dass auch wenn einige Aufgaben eine Ausnahme auslösen, die übrigen weiter ausgeführt werden, und man Ausnahmen beim Abrufen der Ergebnisse einzeln behandeln kann. Mit
Futureaus demconcurrent-Modul ist es wichtig, dass Erfolge und Fehler in der asynchronen Verarbeitung sicher behandelt werden können.
Leitfaden zur Auswahl zwischen Threads und Prozessen
Um Nebenläufigkeit und Parallelität effektiv zu nutzen, ist es wichtig, je nach Aufgabenart die richtige Herangehensweise zu wählen.
In der Praxis helfen folgende Kriterien bei der Entscheidung.
- Für Prozesse mit vielen E/A-Wartezeiten, wie Kommunikation oder Dateioperationen, verwenden Sie
ThreadPoolExecutor. - Für CPU-lastige und rechenintensive Aufgaben verwenden Sie
ProcessPoolExecutor. - Bei vielen einfachen Aufgaben kann die Verwendung von
mapden Code deutlich vereinfachen. - Wenn exakte Kontrolle über die Ausführungsreihenfolge oder Fehlerbehandlung notwendig ist, kombinieren Sie
submitmitas_completed.
Vorteile der Verwendung von concurrent
Mit dem concurrent-Modul können Sie asynchrone Verarbeitung sicher und intuitiv handhaben.
Die wichtigsten Vorteile sind:.
- Sie müssen sich nicht mit Low-Level-Thread- oder Prozessmanagement beschäftigen.
- Es ist Teil der Python-Standardbibliothek, sodass Sie es bedenkenlos einsetzen können.
- Der Code wird lesbarer und leichter wartbar.
- Es eignet sich hervorragend als erster Schritt für das Lernen von Nebenläufigkeit und Parallelität.
Wenn Sie diese Leitlinien beachten, können Sie Fehler bei der Verwendung von concurrent deutlich reduzieren.
Zusammenfassung
Das concurrent-Modul ist die Standardoption für praktische Nebenläufigkeit und Parallelität in Python. Damit können Sie die Leistung steigern, ohne die eigentliche Verarbeitung wesentlich zu verändern – ein großer Vorteil in der Praxis. Mit concurrent können Sie asynchrone Verarbeitung kompakt umsetzen und gleichzeitig die Fehlerbehandlung und Ablaufsteuerung sicher gestalten.
Sie können den obigen Artikel mit Visual Studio Code auf unserem YouTube-Kanal verfolgen. Bitte schauen Sie sich auch den YouTube-Kanal an.