Was Giulio Franco sagt, trifft im Allgemeinen auf Multithreading vs. Multiprocessing zu.
Allerdings hat Python* ein zusätzliches Problem: Es gibt ein globales Interpreter-Sperrobjekt, das verhindert, dass zwei Threads im selben Prozess gleichzeitig Python-Code ausführen. Das bedeutet, wenn Sie 8 Kerne haben und Ihren Code ändern, um 8 Threads zu verwenden, wird er nicht in der Lage sein, 800 % CPU zu nutzen und 8-mal schneller zu laufen; er wird die gleichen 100 % CPU verwenden und mit der gleichen Geschwindigkeit laufen. (In der Realität wird es etwas langsamer laufen, weil es zusätzlichen Overhead durch das Threading gibt, auch wenn Sie keine gemeinsamen Daten haben, aber ignorieren Sie das jetzt.)
Davon gibt es Ausnahmen. Wenn die intensive Berechnung Ihres Codes tatsächlich nicht in Python stattfindet, sondern in einer Bibliothek mit benutzerdefinierter C-Code-Implementierung, die das GIL richtig behandelt, wie zum Beispiel eine Numpy-App, erhalten Sie den erwarteten Leistungsvorteil durch Threading. Das Gleiche gilt, wenn die intensive Berechnung von einem Unterprozess durchgeführt wird, den Sie starten und auf den Sie warten.
Viel wichtiger ist jedoch, dass es Fälle gibt, in denen dies keine Rolle spielt. Beispielsweise verbringt ein Netzwer-Server die meiste Zeit damit, Pakete aus dem Netzwerk zu lesen, und eine GUI-App wartet die meiste Zeit auf Benutzerereignisse. Ein Grund, Threads in einem Netzwerk-Server oder einer GUI-App zu verwenden, besteht darin, länger laufende "Hintergrundaufgaben" durchführen zu können, ohne den Hauptthread daran zu hindern, weiterhin Netzwerkpakete oder GUI-Ereignisse zu verarbeiten. Und das funktioniert mit Python-Threads einwandfrei. (In technischer Hinsicht bedeuten Python-Threads also Parallelität, obwohl sie keine Kerne-Parallelität bieten.)
Aber wenn Sie ein CPU-gebundenes Programm in reinem Python schreiben, sind mehr Threads im Allgemeinen nicht hilfreich.
Die Verwendung separater Prozesse hat keine solchen Probleme mit dem GIL, da jeder Prozess seinen eigenen separaten GIL hat. Natürlich haben Sie immer noch alle gleichen Tradeoffs zwischen Threads und Prozessen wie in anderen Sprachen - es ist schwieriger und teurer, Daten zwischen Prozessen als zwischen Threads zu teilen, es kann teuer sein, eine große Anzahl von Prozessen auszuführen oder sie häufig zu erstellen und zu zerstören etc. Aber das GIL wirkt sich in einem Maße auf die Balance zugunsten von Prozessen aus, das für andere Sprachen wie C oder Java nicht zutrifft. Also werden Sie häufiger Multiprocessing in Python verwenden als in C oder Java.
Unterdessen bringt die "Batterien enthalten" Philosophie von Python einige gute Nachrichten: Es ist sehr einfach, Code zu schreiben, der mit einem Einzeiler zwischen Threads und Prozessen umgeschaltet werden kann.
Wenn Sie Ihren Code in Form von eigenständigen "Jobs" entwerfen, die nichts mit anderen Jobs (oder dem Hauptprogramm) teilen außer Eingabe und Ausgabe, können Sie die concurrent.futures
Bibliothek verwenden, um Ihren Code um einen Threadpool wie folgt zu schreiben:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...
Sie können sogar die Ergebnisse dieser Jobs erhalten und sie an weitere Jobs weitergeben, auf Dinge in der Reihenfolge der Ausführung oder der Fertigstellung warten usw.; lesen Sie den Abschnitt zu Future
-Objekten für Details.
Wenn sich herausstellt, dass Ihr Programm ständig 100 % CPU verwendet und das Hinzufügen von mehr Threads es nur langsamer macht, dann stoßen Sie auf das GIL-Problem, und Sie müssen zu Prozessen wechseln. Alles, was Sie tun müssen, ist diese erste Zeile zu ändern:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
Der einzige wirkliche Haken ist, dass die Argumente und Rückgabewerte Ihrer Jobs picklebar sein müssen (und nicht zu viel Zeit oder Speicher benötigen, um gepickelt zu werden), um sie über Prozesse hinweg verwenden zu können. Normalerweise ist das kein Problem, aber manchmal kommt es vor.
Was aber, wenn Ihre Jobs nicht eigenständig sein können? Wenn Sie Ihren Code in Form von Jobs entwerfen können, die Nachrichten von einem an einen anderen weitergeben, ist es immer noch ziemlich einfach. Möglicherweise müssen Sie threading.Thread
oder multiprocessing.Process
verwenden, anstelle von Pools zu verlassen. Und Sie müssen queue.Queue
oder multiprocessing.Queue
-Objekte explizit erstellen. (Es gibt viele andere Optionen - Pipes, Sockets, Dateien mit Locks, ... aber der Punkt ist, Sie müssen manuell etwas tun, wenn die automatische Magie eines Executors nicht ausreicht.)
Aber was ist, wenn Sie sich nicht einmal auf den Nachrichtenaustausch verlassen können? Was ist, wenn Sie möchten, dass zwei Jobs dieselbe Struktur verändern und die Änderungen des anderen sehen? In diesem Fall müssen Sie manuelle Synchronisation (Sperren, Semaphoren, Bedingungen usw.) und, wenn Sie Prozesse verwenden möchten, explizite gemeinsam genutzte Speicherobjekte verwenden. Hier wird Multithreading (oder Multiprocessing) schwierig. Wenn Sie es vermeiden können, umso besser; wenn nicht, müssen Sie mehr lesen als jemand in eine SO-Antwort packen kann.
In einem Kommentar wollten Sie wissen, was in Python der Unterschied zwischen Threads und Prozessen ist. Wenn Sie die Antwort von Giulio Franco, meine und alle unsere Links lesen, sollten Sie alles abgedeckt haben ... aber eine Zusammenfassung wäre definitiv nützlich, also los geht's:
- Threads teilen standardmäßig Daten; Prozesse nicht.
- Aus Folge von (1) erfordert das Senden von Daten zwischen Prozessen in der Regel deren Ein- und Auspacken.
- Auch aus Folge von (1) erfordert das direkte Teilen von Daten zwischen Prozessen im Allgemeinen, dass es in Rohdatenformaten wie "Value", "Array" und
ctypes
-Typen abgelegt wird.
- Prozesse sind nicht dem GIL unterworfen.
- Auf einigen Plattformen (hauptsächlich Windows) sind Prozesse wesentlich teurer zu erstellen und zu zerstören.
- Es gibt einige zusätzliche Einschränkungen für Prozesse, die auf verschiedenen Plattformen unterschiedlich sind. Details finden Sie in den Programmierungsrichtlinien.
- Das
threading
-Modul verfügt nicht über einige der Funktionen des multiprocessing
-Moduls. (Sie können multiprocessing.dummy
verwenden, um einen Großteil der fehlenden API auf Basis von Threads zu erhalten, oder Sie können höherstufige Module wie concurrent.futures
verwenden und sich keine Gedanken darüber machen.)
* Es ist nicht wirklich Python, die Sprache, die dieses Problem hat, sondern CPython, die "Standard"-Implementierung dieser Sprache. Einige andere Implementierungen haben keinen GIL, wie zum Beispiel Jython.
** Wenn Sie die "fork"-Startmethode für Multiprocessing verwenden - was Sie auf den meisten nicht-Windows-Plattformen können - erhält jeder Kindprozess alle Ressourcen, die der Elternprozess hatte, als das Kind gestartet wurde, was eine andere Möglichkeit darstellt, Daten an Kinder weiterzugeben.