Ich lerne, wie ich die threading
und die multiprocessing
Module in Python verwende, um bestimmte Operationen parallel auszuführen und meinen Code zu beschleunigen.
Ich finde es schwierig (vielleicht weil ich keinen theoretischen Hintergrund dazu habe) zu verstehen, was der Unterschied zwischen einem threading.Thread()
Objekt und einem ist multiprocessing.Process()
.
Außerdem ist mir nicht ganz klar, wie ich eine Warteschlange von Jobs instanziieren und nur 4 (zum Beispiel) parallel ausführen kann, während die anderen darauf warten, dass Ressourcen freigegeben werden, bevor sie ausgeführt werden.
Ich finde die Beispiele in der Dokumentation klar, aber nicht sehr erschöpfend; Sobald ich versuche, die Dinge etwas zu komplizieren, erhalte ich viele seltsame Fehler (wie eine Methode, die nicht eingelegt werden kann, und so weiter).
Wann sollte ich die Module threading
und verwenden multiprocessing
?
Können Sie mich mit einigen Ressourcen verknüpfen, in denen die Konzepte hinter diesen beiden Modulen erläutert werden und wie sie für komplexe Aufgaben richtig verwendet werden können?
Thread
Modul (_thread
in Python 3.x genannt). Um ehrlich zu sein, habe ich die Unterschiede selbst nie verstanden ...Thread
/_thread
Dokumentation ausdrücklich angegeben, handelt es sich um " Grundelemente auf niedriger Ebene". Sie können es verwenden, um benutzerdefinierte Synchronisationsobjekte zu erstellen, die Verknüpfungsreihenfolge eines Thread-Baums zu steuern usw. Wenn Sie sich nicht vorstellen können, warum Sie es verwenden müssen, verwenden Sie es nicht und bleiben Sie dabeithreading
.Antworten:
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: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:
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 sichmultiprocessing.Process
nicht darauf verlassen. Und Sie müssenqueue.Queue
odermultiprocessing.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:
ctypes
Typen gebracht werden.threading
Modul verfügt nicht über einige Funktionen desmultiprocessing
Moduls. (Sie können verwendenmultiprocessing.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.
quelle
pickle
Dokumente erklären), und dann im schlimmsten Fall Ihr dummer Wrapper istdef wrapper(obj, *args): return obj.wrapper(*args)
.In einem Prozess können mehrere Threads vorhanden sein. Die Threads, die zu demselben Prozess gehören, teilen sich denselben Speicherbereich (können aus denselben Variablen lesen und in diese schreiben und sich gegenseitig stören). Im Gegenteil, verschiedene Prozesse leben in verschiedenen Speicherbereichen, und jeder von ihnen hat seine eigenen Variablen. Um zu kommunizieren, müssen Prozesse andere Kanäle (Dateien, Pipes oder Sockets) verwenden.
Wenn Sie eine Berechnung parallelisieren möchten, benötigen Sie wahrscheinlich Multithreading, da die Threads wahrscheinlich im selben Speicher zusammenarbeiten sollen.
In Bezug auf die Leistung lassen sich Threads schneller erstellen und verwalten als Prozesse (da das Betriebssystem keinen neuen virtuellen Speicherbereich zuweisen muss), und die Kommunikation zwischen Threads ist normalerweise schneller als die Kommunikation zwischen Prozessen. Threads sind jedoch schwieriger zu programmieren. Threads können sich gegenseitig stören und in den Speicher des anderen schreiben. Die Art und Weise, wie dies geschieht, ist jedoch nicht immer offensichtlich (aufgrund verschiedener Faktoren, hauptsächlich der Neuanordnung von Anweisungen und des Zwischenspeicherns von Speicher). Daher benötigen Sie Synchronisationsprimitive, um den Zugriff zu steuern zu Ihren Variablen.
quelle
threading
und Python istmultiprocessing
.Ich glaube, dieser Link beantwortet Ihre Frage auf elegante Weise.
Um es kurz zu machen: Wenn eines Ihrer Unterprobleme warten muss, bis ein anderes beendet ist, ist Multithreading gut (z. B. bei schweren E / A-Vorgängen). Im Gegensatz dazu wird Multiprocessing empfohlen, wenn Ihre Unterprobleme tatsächlich gleichzeitig auftreten können. Sie werden jedoch nicht mehr Prozesse als Ihre Anzahl von Kernen erstellen.
quelle
Zitate zur Python-Dokumentation
Ich habe die wichtigsten Python-Dokumentationszitate zu Process vs Threads und der GIL unter folgender Adresse hervorgehoben: Was ist die globale Interpretersperre (GIL) in CPython?
Prozess gegen Thread-Experimente
Ich habe ein bisschen Benchmarking durchgeführt, um den Unterschied konkreter darzustellen.
Im Benchmark habe ich die CPU- und E / A-gebundene Arbeit für verschiedene Anzahlen von Threads auf einer 8-Hyperthread- CPU zeitgesteuert . Die pro Thread gelieferte Arbeit ist immer gleich, sodass mehr Threads mehr Gesamtarbeit bedeuten.
Die Ergebnisse waren:
Plotdaten .
Schlussfolgerungen:
Bei CPU-gebundener Arbeit ist die Mehrfachverarbeitung immer schneller, vermutlich aufgrund der GIL
für IO-gebundene Arbeit. beide sind genau gleich schnell
Threads skalieren nur bis zu ungefähr 4x anstelle der erwarteten 8x, da ich auf einer 8-Hyperthread-Maschine bin.
Vergleichen Sie dies mit einer C POSIX-CPU-gebundenen Arbeit, die die erwartete 8-fache Geschwindigkeit erreicht: Was bedeuten "real", "user" und "sys" in der Ausgabe der Zeit (1)?
TODO: Ich kenne den Grund dafür nicht, es müssen andere Python-Ineffizienzen ins Spiel kommen.
Testcode:
GitHub Upstream + Plotten von Code im selben Verzeichnis .
Getestet unter Ubuntu 18.10, Python 3.6.7, in einem Lenovo ThinkPad P51-Laptop mit CPU: Intel Core i7-7820HQ-CPU (4 Kerne / 8 Threads), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD: Samsung MZVLB512HAJQ- 000L7 (3.000 MB / s).
Visualisieren Sie, welche Threads zu einem bestimmten Zeitpunkt ausgeführt werden
In diesem Beitrag unter https://rohanvarma.me/GIL/ habe ich gelernt, dass Sie einen Rückruf ausführen können, wenn ein Thread mit dem
target=
Argument vonthreading.Thread
und dasselbe für geplant istmultiprocessing.Process
.Auf diese Weise können wir genau anzeigen, welcher Thread zu jedem Zeitpunkt ausgeführt wird. Wenn dies erledigt ist, würden wir so etwas sehen (ich habe dieses spezielle Diagramm erstellt):
was zeigen würde, dass:
quelle
Hier sind einige Leistungsdaten für Python 2.6.x, die die Vorstellung in Frage stellen, dass Threading in IO-gebundenen Szenarien leistungsfähiger ist als Multiprocessing. Diese Ergebnisse stammen von einem IBM System x3650 M4 BD mit 40 Prozessoren.
E / A-gebundene Verarbeitung: Der Prozesspool hat eine bessere Leistung als der Thread-Pool
CPU-gebundene Verarbeitung: Der Prozesspool schnitt besser ab als der Thread-Pool
Dies sind keine strengen Tests, aber sie sagen mir, dass Multiprocessing im Vergleich zu Threading nicht ganz unperformant ist.
Code, der in der interaktiven Python-Konsole für die oben genannten Tests verwendet wird
quelle
>>> do_work(50, 300, 'thread', 'fileio') --> 237.557 ms
>>> do_work(50, 300, 'process', 'fileio') --> 323.963 ms
>>> do_work(50, 2000, 'thread', 'square') --> 232.082 ms
>>> do_work(50, 2000, 'process', 'square') --> 282.785 ms
Nun, der größte Teil der Frage wird von Giulio Franco beantwortet. Ich werde weiter auf das Verbraucher-Hersteller-Problem eingehen, das Sie vermutlich auf den richtigen Weg für Ihre Lösung für die Verwendung einer Multithread-App bringen wird.
Weitere Informationen zu den Synchronisationsprimitiven finden Sie unter:
Der Pseudocode ist oben. Ich nehme an, Sie sollten das Produzent-Verbraucher-Problem durchsuchen, um weitere Referenzen zu erhalten.
quelle