Was Giulio Franco sagt, gilt für Multithreading vs. Multiprocessing im Allgemeinen .
Python * hat jedoch ein zusätzliches Problem: Es gibt eine globale Interpretersperre, die verhindert, dass zwei Threads im selben Prozess gleichzeitig Python-Code ausführen. Dies bedeutet, dass wenn Sie 8 Kerne haben und Ihren Code so ändern, dass 8 Threads verwendet werden, 800% der CPU nicht verwendet werden können und 8x schneller ausgeführt werden kann. Es wird dieselbe 100% ige CPU verwenden und mit derselben Geschwindigkeit ausgeführt. (In Wirklichkeit läuft es etwas langsamer, da das Threading zusätzlichen Aufwand verursacht, selbst wenn Sie keine gemeinsam genutzten Daten haben, aber ignorieren Sie dies vorerst.)
Hiervon gibt es Ausnahmen. Wenn die umfangreiche Berechnung Ihres Codes nicht in Python erfolgt, sondern in einer Bibliothek mit benutzerdefiniertem C-Code, die eine ordnungsgemäße GIL-Behandlung ausführt, wie z. B. einer Numpy-App, erhalten Sie den erwarteten Leistungsvorteil durch Threading. Das Gleiche gilt, wenn die umfangreiche Berechnung von einem Unterprozess durchgeführt wird, den Sie ausführen und auf den Sie warten.
Noch wichtiger ist, dass es Fälle gibt, in denen dies keine Rolle spielt. Beispielsweise verbringt ein Netzwerkserver die meiste Zeit damit, Pakete aus dem Netzwerk zu lesen, und eine GUI-App verbringt die meiste Zeit damit, auf Benutzerereignisse zu warten. Ein Grund für die Verwendung von Threads in einem Netzwerkserver oder einer GUI-App besteht darin, dass Sie lang laufende "Hintergrundaufgaben" ausführen können, ohne den Hauptthread daran zu hindern, weiterhin Netzwerkpakete oder GUI-Ereignisse zu bedienen. Und das funktioniert gut mit Python-Threads. (In technischer Hinsicht bedeutet dies, dass Python-Threads Ihnen Parallelität bieten, obwohl sie Ihnen keine Kernparallelität bieten.)
Wenn Sie jedoch ein CPU-gebundenes Programm in reinem Python schreiben, ist die Verwendung weiterer Threads im Allgemeinen nicht hilfreich.
Die Verwendung separater Prozesse hat keine derartigen Probleme mit der GIL, da jeder Prozess seine eigene separate GIL hat. Natürlich haben Sie immer noch dieselben Kompromisse zwischen Threads und Prozessen wie in allen anderen Sprachen - es ist schwieriger und teurer, Daten zwischen Prozessen auszutauschen als zwischen Threads. Es kann kostspielig sein, eine große Anzahl von Prozessen auszuführen oder zu erstellen und zu zerstören sie häufig usw. Aber die GIL belastet das Gleichgewicht in Bezug auf Prozesse auf eine Weise, die beispielsweise für C oder Java nicht zutrifft. Daher werden Sie in Python viel häufiger Multiprocessing verwenden als in C oder Java.
In der Zwischenzeit bringt Pythons Philosophie "Batterien enthalten" einige gute Neuigkeiten: Es ist sehr einfach, Code zu schreiben, der mit einem einzeiligen Wechsel zwischen Threads und Prozessen hin- und hergeschaltet werden kann.
Wenn Sie Ihren Code in eigenständigen "Jobs" entwerfen, die nichts mit anderen Jobs (oder dem Hauptprogramm) außer Eingabe und Ausgabe teilen, können Sie die concurrent.futures
Bibliothek verwenden, um Ihren Code um einen Thread-Pool 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 abrufen und an weitere Jobs weitergeben, auf Dinge in der Reihenfolge ihrer Ausführung oder in der Reihenfolge ihrer Fertigstellung warten usw.; Lesen Sie den Abschnitt über Future
Objekte für Details.
Wenn sich herausstellt, dass Ihr Programm ständig 100% CPU verwendet und das Hinzufügen weiterer Threads nur langsamer wird, tritt das GIL-Problem auf, sodass Sie zu Prozessen wechseln müssen. Alles was Sie tun müssen, ist diese erste Zeile zu ändern:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
Die einzige wirkliche Einschränkung besteht darin, dass die Argumente und Rückgabewerte Ihrer Jobs abwählbar sein müssen (und nicht zu viel Zeit oder Speicher benötigen, um abgehackt zu werden), um prozessübergreifend verwendet werden zu können. Normalerweise ist dies kein Problem, aber manchmal ist es das auch.
Aber was ist, wenn Ihre Jobs nicht in sich geschlossen sein können? Wenn Sie Ihren Code in Form von Jobs entwerfen können, die Nachrichten von einem zum anderen weitergeben, ist dies immer noch recht einfach. Möglicherweise müssen Sie Pools verwenden threading.Thread
oder sich multiprocessing.Process
nicht darauf verlassen. Und Sie müssen queue.Queue
oder multiprocessing.Queue
Objekte explizit erstellen . (Es gibt viele andere Optionen - Pipes, Sockets, Dateien mit Herden ... aber der Punkt ist, dass Sie etwas manuell tun müssen, wenn die automatische Magie eines Executors nicht ausreicht.)
Aber was ist, wenn Sie sich nicht einmal auf die Weitergabe von Nachrichten verlassen können? Was ist, wenn Sie zwei Jobs benötigen, um dieselbe Struktur zu mutieren und die Änderungen der anderen zu sehen? In diesem Fall müssen Sie eine manuelle Synchronisierung (Sperren, Semaphoren, Bedingungen usw.) durchführen und, wenn Sie Prozesse verwenden möchten, explizite Shared-Memory-Objekte zum Booten. Dies ist der Fall, wenn Multithreading (oder Multiprocessing) schwierig wird. Wenn Sie es vermeiden können, großartig; Wenn Sie nicht können, müssen Sie mehr lesen, als jemand in eine SO-Antwort eingeben kann.
In einem Kommentar wollten Sie wissen, was sich zwischen Threads und Prozessen in Python unterscheidet. Wirklich, wenn Sie die Antwort von Giulio Franco und meine und alle unsere Links lesen, sollte das alles abdecken… aber eine Zusammenfassung wäre definitiv nützlich, also hier ist:
- Threads teilen standardmäßig Daten. Prozesse nicht.
- Infolge von (1) erfordert das Senden von Daten zwischen Prozessen im Allgemeinen das Beizen und Entfernen von Daten. ** **.
- Als weitere Konsequenz von (1) erfordert der direkte Austausch von Daten zwischen Prozessen im Allgemeinen, dass diese in Formate auf niedriger Ebene wie Wert, Array und
ctypes
Typen gebracht werden.
- Prozesse unterliegen nicht der GIL.
- Auf einigen Plattformen (hauptsächlich Windows) ist das Erstellen und Zerstören von Prozessen viel teurer.
- Es gibt einige zusätzliche Einschränkungen für Prozesse, von denen einige auf verschiedenen Plattformen unterschiedlich sind. Einzelheiten finden Sie in den Programmierrichtlinien .
- Das
threading
Modul verfügt nicht über einige Funktionen des multiprocessing
Moduls. (Sie können verwenden multiprocessing.dummy
, um den größten Teil der fehlenden API über Threads zu erhalten, oder Sie können übergeordnete Module verwenden, concurrent.futures
ohne sich darum zu kümmern.)
* Es ist nicht Python, die Sprache, die dieses Problem hat, sondern CPython, die "Standard" -Implementierung dieser Sprache. Einige andere Implementierungen haben keine GIL, wie Jython.
** Wenn Sie die Fork- Start-Methode für die Mehrfachverarbeitung verwenden, die Sie auf den meisten Nicht-Windows-Plattformen verwenden können, erhält jeder untergeordnete Prozess alle Ressourcen, über die die Eltern beim Starten des Kindes verfügten. Dies kann eine weitere Möglichkeit sein, Daten an Kinder zu übergeben.
Thread
Modul (_thread
in Python 3.x genannt). Um ehrlich zu sein, habe ich die Unterschiede selbst nie verstanden ...