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.