Einführung in Multithreading, Multiprocessing und async#
Martelli’s Modell der Skalierbarkeit#
Anzahl Kerne |
Beschreibung |
---|---|
1 |
Einzelner Thread und einzelner Prozess |
2–8 |
Mehrere Threads und mehrere Prozesse |
>8 |
Verteilte Verarbeitung |
Martelli’s Beobachtung war, dass im Laufe der Zeit die zweite Kategorie immer unbedeutender wird, da einzelne Kerne werden immer leistungsfähiger und große Datensätze immer größer werden.
Global Interpreter Lock (GIL)#
CPython verfügt über eine Sperre für seinen intern geteilten globalen Status. Dies hat zur Folge, dass nicht mehr als ein Thread gleichzeitig laufen kann.
Für I/O-lastige Anwendungen ist das GIL kein großes Problem; bei CPU-lastigen Anwendungen führt die Verwendung von Threading jedoch zu einer Verlangsamung. Dementsprechend ist Multi-Processing für uns spannend um mehr CPU-Zyklen zu erhalten.
Literate programming und Martelli’s Modell der Skalierbarkeit bestimmten die Design-Entscheidungen zur Performance von Python über lange Zeit. An dieser Einschätzung hat sich bis heute wenig geändert: Entgegen der intuitiven Erwartungen führen mehr CPUs und Threads in Python zunächst zu weniger effizienten Anwendungen. Dennoch wünschen sich laut der Umfrage von 2020 zu den gewünschten Python-Features 20% Performance-Verbesserungen und 15% bessere Nebenläufigkeit und Parallelisierung. Das Gilectomy-Projekt, das das GIL ersetzen sollte, stieß jedoch auch noch auf ein weiteres Problem: Die Python C-API legt sehr viele Implementierungsdetails offen. Damit würden Leistungsverbesserungen jedoch schnell zu inkompatiblen Änderungen führen, die dann vor allem bei einer so beliebten Sprache wie Python inakzeptabel erscheinen.
Überblick#
Kriterium |
Multithreading |
Multiprocessing |
asyncio |
---|---|---|---|
Trennung |
Threads teilen sich einen Status. Das Teilen eines Status kann jedoch zu Race Conditions führen, d.h. die Ergebnisse einer Operation können vom zeitlichen Verhalten bestimmter Einzeloperationen abhängen. |
Die Prozesse sind unabhängig voneinander. Sollen sie dennoch miteinander kommunizieren, wird Interprocess communication (IPC), object pickling und anderer Overhead nötig. |
Mit
Fast alle |
Wechsel |
Threads wechseln präemptiv, d.h., es muss kein expliziter Code hinzugefügt werdenm um einen Wechsel der Tasks zu verursachen. Ein solcher
Wechsel ist
jedoch jederzeit
möglich;
dementsprechend
müssen kritische
Bereiche mit
|
Sobald ihr den Prozess erhaltet, sollten deutliche Fortschritte gemacht werden. Ihr solltet also nicht zu viele Roundtrips hin und her machen. |
asyncio wechselt kooperativ, d.h., es muss explizit yield oder await angegeben werden um einen Wechsel herbeizuführen. Ihr könnt daher die Aufwände für diese Wechsel sehr gering halten. |
Tooling |
Threads erfordern sehr wenig Tooling: Lock und Queue. Locks sind in nicht-trivialen Beispielen schwer zu verstehen. Bei komplexen Anwendungen sollten daher besser atomare Message Queues oder asyncio verwendet werden. |
Einfaches Tooling u.a. mit map und imap_unordered um einzelne Prozesse in einem einzelnen Thread zu testen, bevor zu Multiprocessing gewechselt wird. Wird IPC oder object pickling verwendet, wird das Tooling jedoch aufwändiger. |
Zumindest bei komplexen
Systemen führt
|
Performance |
Multithreading führt bei IO-lastigen Aufgaben zu den besten Ergebnissen. Die Leistungsgrenze für Threads ist eine CPU abzüglich der Kosten für Task-Switches und Aufwänden für die Synchronisation. |
Die Prozesse können auf mehrere CPUs verteilt werden und sollten daher für CPU-lastige Aufgaben verwendet werden. Für die Kommunikation und die Synchronisierung der Prozesse entstehen jedoch ggf. zusätzliche Aufwände. |
Der Aufruf einer reinen
Python-Funktion erzeugt mehr
Overhead als die erneute
Anfrage eines Für CPU-intensive Aufgaben ist jedoch Multiprocessing besser geeignet. |
Resümee#
Es gibt nicht die eine ideale Implementierung von Nebenläufigkeit – jeder der im folgenden vorgestellten Ansätze hat spezifische Vor- und Nachteile. Bevor ihr euch also entscheidet, welchen Ansatz ihr verfolgen wollt, solltet ihr die Performance-Probleme genau analysieren und anschließend die jeweils passende Läsung wählen. In unseren Projekten verwenden wir dabei häufig mehrere Ansätze, je nachdem, für welchen Teil die Performance optimiert werden soll.