.. SPDX-FileCopyrightText: 2021 cusy GmbH
..
.. SPDX-License-Identifier: BSD-3-Clause
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 | Die Prozesse sind| Mit |
| | sich einen | unabhängig | ``run_coroutine_threadsafe()`` |
| | Status. | voneinander. | können ``asyncio``-Objekte |
| | | | auch von anderen Threads |
| | Das Teilen eines | Sollen sie | verwendet werden. |
| | Status kann | dennoch | |
| | jedoch zu *Race | miteinander | Fast alle ``asyncio``-Objekte |
| | Conditions* | kommunizieren, | sind nicht threadsicher. |
| | führen, d.h. die | wird | |
| | Ergebnisse einer | `Interprocess | |
| | Operation können | communication | |
| | vom zeitlichen | (IPC)`_, | |
| | Verhalten | `object | |
| | bestimmter | pickling`_ und | |
| | Einzeloperationen| anderer Overhead | |
| | abhängen. | nötig. | |
+------------------+------------------+------------------+--------------------------------+
| Wechsel | Threads wechseln | Sobald ihr den | asyncio wechselt `kooperativ`_,|
| | `präemptiv`_, | Prozess erhaltet,| d.h., es muss explizit `yield`_|
| | d.h., es muss | sollten deutliche| oder `await`_ angegeben werden |
| | kein expliziter | Fortschritte | um einen Wechsel |
| | Code hinzugefügt | gemacht werden. | herbeizuführen. Ihr könnt daher|
| | werdenm um einen | Ihr solltet also | die Aufwände für diese Wechsel |
| | Wechsel der Tasks| nicht zu viele | sehr gering halten. |
| | zu verursachen. | Roundtrips hin | |
| | | und her machen. | |
| | Ein solcher | | |
| | Wechsel ist | | |
| | jedoch jederzeit | | |
| | möglich; | | |
| | dementsprechend | | |
| | müssen kritische | | |
| | Bereiche mit | | |
| | ``lock`` | | |
| | geschützt werden.| | |
| | | | |
| | | | |
+------------------+------------------+------------------+--------------------------------+
| Tooling | Threads erfordern| Einfaches Tooling| Zumindest bei komplexen |
| | sehr wenig | u.a. mit `map`_ | Systemen führt ``asyncio`` |
| | Tooling: `Lock`_ | und | einfacher zum Ziel als |
| | und `Queue`_. | `imap_unordered`_| Multithreading Locks. |
| | | um einzelne | |
| | Locks sind in | Prozesse in einem| ``asyncio`` benötigt jedoch |
| | nicht-trivialen | einzelnen Thread | eine große Menge von |
| | Beispielen schwer| zu testen, bevor | Werkzeugen: `futures`_, |
| | zu verstehen. | zu | `Event Loops`_ und nicht |
| | Bei komplexen | Multiprocessing | blockierende Versionen von fast|
| | Anwendungen | gewechselt wird. | allem. |
| | sollten daher | | |
| | besser atomare | Wird IPC oder | |
| | Message Queues | object pickling | |
| | oder asyncio | verwendet, wird | |
| | verwendet werden.| das Tooling | |
| | | jedoch | |
| | | aufwändiger. | |
+------------------+------------------+------------------+--------------------------------+
| Performance | Multithreading | Die Prozesse | Der Aufruf einer reinen |
| | führt bei | können auf | Python-Funktion erzeugt mehr |
| | IO-lastigen | mehrere CPUs | Overhead als die erneute |
| | Aufgaben zu den | verteilt werden | Anfrage eines ``generator`` |
| | besten | und sollten daher| oder ``awaitable`` – d.h., |
| | Ergebnissen. | für CPU-lastige | ``asyncio`` kann die CPU |
| | | Aufgaben | effizient auslasten. |
| | Die | verwendet werden.| |
| | Leistungsgrenze | | Für CPU-intensive Aufgaben ist |
| | für Threads ist | Für die | jedoch Multiprocessing besser |
| | eine CPU | Kommunikation und| geeignet. |
| | abzüglich der | die | |
| | Kosten für | Synchronisierung | |
| | Task-Switches | der Prozesse | |
| | und | entstehen jedoch | |
| | Aufwänden für die| ggf. zusätzliche | |
| | Synchronisation. | Aufwände. | |
+------------------+------------------+------------------+--------------------------------+
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.
.. _`Interprocess Communication (IPC)`: https://docs.python.org/3/library/ipc.html
.. _`object pickling`: https://docs.python.org/3/library/pickle.html
.. _`präemptiv`: https://de.wikipedia.org/wiki/Multitasking#Pr%C3%A4emptives_Multitasking
.. _`Lock`: https://docs.python.org/3/library/threading.html#threading.Lock
.. _`Queue`: https://docs.python.org/3/library/queue.html
.. _`kooperativ`: https://de.wikipedia.org/wiki/Multitasking#Kooperatives_Multitasking
.. _`yield`: https://docs.python.org/3/reference/simple_stmts.html#yield
.. _`await`: https://docs.python.org/3/reference/expressions.html#await
.. _`map`: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.map
.. _`imap_unordered`: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.imap_unordered
.. _`futures`: https://docs.python.org/3/library/asyncio-task.html#awaitables
.. _`Event Loops`: https://docs.python.org/3/library/asyncio-eventloop.html