PEP 371 – Aufnahme des multiprocessing-Pakets in die Standardbibliothek
- Autor:
- Jesse Noller <jnoller at gmail.com>, Richard Oudkerk <r.m.oudkerk at googlemail.com>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 06. Mai 2008
- Python-Version:
- 2.6, 3.0
- Post-History:
- 03. Juni 2008
Zusammenfassung
Diese PEP schlägt die Aufnahme des pyProcessing-Pakets [1] in die Python-Standardbibliothek vor, umbenannt in „multiprocessing“.
Das processing-Paket ahmt die Funktionalität des threading-Moduls der Standardbibliothek nach, um einen prozessbasierten Ansatz für das Threading zu bieten, der es Endbenutzern ermöglicht, mehrere Aufgaben zu verteilen, die den globalen Interpreter-Lock effektiv umgehen.
Das Paket bietet auch Server- und Client-Funktionalität (processing.Manager), um die gemeinsame Nutzung und Verwaltung von Objekten und Aufgaben über Fernzugriff zu ermöglichen, sodass Anwendungen nicht nur mehrere Kerne auf dem lokalen Rechner nutzen können, sondern auch Objekte und Aufgaben über einen Cluster vernetzter Rechner verteilen können.
Während die verteilten Fähigkeiten des Pakets von Vorteil sind, liegt der Schwerpunkt dieser PEP auf der Kern-API und den Funktionen, die mit dem Threading vergleichbar sind.
Begründung
Der aktuelle CPython-Interpreter implementiert den Global Interpreter Lock (GIL) und vorbehaltlich von Arbeiten in Python 3000 oder anderen derzeit geplanten Versionen [2] wird der GIL im CPython-Interpreter auf absehbare Zeit unverändert bleiben. Während der GIL selbst sauberen und leicht zu wartenden C-Code für den Interpreter und die Erweiterungsbasis ermöglicht, ist er häufig ein Problem für Python-Programmierer, die Mehrkernmaschinen nutzen.
Der GIL selbst verhindert, dass mehr als ein Thread zu einem bestimmten Zeitpunkt innerhalb des Interpreters läuft, was die Fähigkeit von Python, Mehrprozessorsysteme zu nutzen, effektiv zunichtemacht.
Das pyprocessing-Paket bietet eine Methode, den GIL zu umgehen, wodurch Anwendungen innerhalb von CPython Mehrkernarchitekturen nutzen können, ohne dass Benutzer ihr Programmierparadigma vollständig ändern müssen (d. h. das Threading für einen anderen „konvergenten“ Ansatz aufgeben – Twisted, Actors usw.).
Das Processing-Paket bietet CPython eine „bekannte API“, die, wenn auch in einer PEP 8-konformen Weise, der des Threading-APIs mit bekannten Semantik und einfacher Skalierbarkeit ähnelt.
In Zukunft ist das Paket möglicherweise nicht mehr so relevant, sollte der CPython-Interpreter „echtes“ Threading ermöglichen. Für einige Anwendungen kann das Forken eines Betriebssystemprozesses jedoch manchmal wünschenswerter sein als die Verwendung von leichten Threads, insbesondere auf Plattformen, auf denen die Prozesserstellung schnell und optimiert ist.
Zum Beispiel eine einfache Thread-Anwendung
from threading import Thread as worker
def afunc(number):
print number * 3
t = worker(target=afunc, args=(4,))
t.start()
t.join()
Das pyprocessing-Paket spiegelte die API so gut wider, dass mit einer einfachen Änderung des Imports zu
from processing import process as worker
Der Code würde jetzt über die processing.process-Klasse ausgeführt werden. Offensichtlich wäre mit der Umbenennung der API zur PEP 8-Konformität eine zusätzliche Umbenennung in Benutzeranwendungen erforderlich, die jedoch geringfügig ist.
Diese Art der Kompatibilität bedeutet, dass Benutzeranwendungen mit einer in den meisten Fällen geringfügigen Codeänderung alle Kerne und Prozessoren eines gegebenen Rechners für die parallele Ausführung nutzen können. In vielen Fällen ist das pyprocessing-Paket für I/O-gebundene Programme sogar schneller als der normale Threading-Ansatz. Dies unter Berücksichtigung der Tatsache, dass das pyprocessing-Paket in optimiertem C-Code vorliegt, während das threading-Modul dies nicht tut.
Das „verteilte“ Problem
In der Diskussion auf Python-Dev über die Aufnahme dieses Pakets [3] gab es Verwirrung über die Absichten dieser PEP, da versucht wurde, das „verteilte“ Problem zu lösen – häufig wurde die Funktionalität dieses Pakets mit anderen Lösungen wie MPI-basierter Kommunikation [4], CORBA oder anderen verteilten Objektansätzen verglichen [5].
Das „verteilte“ Problem ist groß und vielfältig. Jeder Programmierer, der in diesem Bereich tätig ist, hat entweder sehr starke Meinungen zu seinem bevorzugten Modul/Methode oder ein hochgradig angepasstes Problem, für das keine bestehende Lösung funktioniert.
Die Annahme dieses Pakets schließt nicht aus oder empfiehlt nicht, dass Programmierer, die am „verteilten“ Problem arbeiten, keine anderen Lösungen für ihren Problembereich untersuchen. Die Absicht der Aufnahme dieses Pakets ist es, Einstiegsfähigkeiten für lokale Konvergenz und grundlegende Unterstützung zur Verbreitung dieser Konvergenz über ein Computernetzwerk bereitzustellen – obwohl die beiden nicht eng gekoppelt sind, könnte das pyprocessing-Paket tatsächlich in Verbindung mit jeder der anderen Lösungen, einschließlich MPI/usw., verwendet werden.
Bei Bedarf ist es möglich, die lokalen Konvergenzfähigkeiten des Pakets vollständig von den netzwerkfähigen/gemeinsamen Aspekten des Pakets zu entkoppeln. Ohne ernsthafte Bedenken oder Ursachen empfiehlt der Autor dieser PEP diesen Ansatz jedoch nicht.
Leistungsvergleich
Wie wir alle wissen, gibt es „Lügen, verdammte Lügen und Benchmarks“. Diese Geschwindigkeitsvergleiche, obwohl sie die Leistung des pyprocessing-Pakets hervorheben sollen, sind keineswegs umfassend oder für alle möglichen Anwendungsfälle oder Umgebungen relevant. Insbesondere für Plattformen mit trägen Prozess-Forking-Zeiten.
Alle Benchmarks wurden mit folgenden Angaben durchgeführt:
- 4-Kern Intel Xeon CPU @ 3,00 GHz
- 16 GB RAM
- Python 2.5.2, kompiliert auf Gentoo Linux (Kernel 2.6.18.6)
- pyProcessing 0.52
Der gesamte Code hierfür kann von http://jessenoller.com/code/bench-src.tgz heruntergeladen werden.
Die grundlegende Ausführungsmethode dieser Benchmarks ist das Skript run_benchmarks.py [6], das einfach als Wrapper dient, um eine Ziel-Funktion über eine einzelne Thread (linear), mehrere Threads (via threading) und mehrere Prozesse (via pyprocessing) für eine feste Anzahl von Iterationen mit steigender Anzahl von Ausführungsschleifen und/oder Threads auszuführen.
Das Skript run_benchmarks.py führt jede Funktion 100 Mal aus und wählt den besten Lauf dieser 100 Iterationen über das timeit-Modul aus.
Zuerst, um den Overhead des Spawns der Worker zu identifizieren, führen wir eine Funktion aus, die einfach eine pass-Anweisung (leer) ist.
cmd: python run_benchmarks.py empty_func.py
Importing empty_func
Starting tests ...
non_threaded (1 iters) 0.000001 seconds
threaded (1 threads) 0.000796 seconds
processes (1 procs) 0.000714 seconds
non_threaded (2 iters) 0.000002 seconds
threaded (2 threads) 0.001963 seconds
processes (2 procs) 0.001466 seconds
non_threaded (4 iters) 0.000002 seconds
threaded (4 threads) 0.003986 seconds
processes (4 procs) 0.002701 seconds
non_threaded (8 iters) 0.000003 seconds
threaded (8 threads) 0.007990 seconds
processes (8 procs) 0.005512 seconds
Wie Sie sehen, ist das Prozess-Forking über das pyprocessing-Paket schneller als die Geschwindigkeit des Aufbaus und der anschließenden Ausführung der Thread-Version des Codes.
Der zweite Test berechnet 50.000 Fibonacci-Zahlen innerhalb jedes Threads (isoliert und nichts gemeinsam).
cmd: python run_benchmarks.py fibonacci.py
Importing fibonacci
Starting tests ...
non_threaded (1 iters) 0.195548 seconds
threaded (1 threads) 0.197909 seconds
processes (1 procs) 0.201175 seconds
non_threaded (2 iters) 0.397540 seconds
threaded (2 threads) 0.397637 seconds
processes (2 procs) 0.204265 seconds
non_threaded (4 iters) 0.795333 seconds
threaded (4 threads) 0.797262 seconds
processes (4 procs) 0.206990 seconds
non_threaded (8 iters) 1.591680 seconds
threaded (8 threads) 1.596824 seconds
processes (8 procs) 0.417899 seconds
Der dritte Test berechnet die Summe aller Primzahlen unter 100.000, wiederum ohne gemeinsame Nutzung.
cmd: run_benchmarks.py crunch_primes.py
Importing crunch_primes
Starting tests ...
non_threaded (1 iters) 0.495157 seconds
threaded (1 threads) 0.522320 seconds
processes (1 procs) 0.523757 seconds
non_threaded (2 iters) 1.052048 seconds
threaded (2 threads) 1.154726 seconds
processes (2 procs) 0.524603 seconds
non_threaded (4 iters) 2.104733 seconds
threaded (4 threads) 2.455215 seconds
processes (4 procs) 0.530688 seconds
non_threaded (8 iters) 4.217455 seconds
threaded (8 threads) 5.109192 seconds
processes (8 procs) 1.077939 seconds
Der Grund, warum sich die Tests zwei und drei auf reine numerische Berechnungen konzentrierten, ist die Darstellung, wie die aktuelle Threading-Implementierung Nicht-I/O-Anwendungen behindert. Offensichtlich könnten diese Tests verbessert werden, um eine Warteschlange für die Koordination von Ergebnissen und Arbeitsblöcken zu verwenden, aber das ist nicht erforderlich, um die Leistung des Pakets und des Kern-processing.process-Moduls zu zeigen.
Der nächste Test ist ein I/O-gebundener Test. Hier sehen wir normalerweise eine deutliche Verbesserung des Threading-Modulansatzes gegenüber einem Single-Thread-Ansatz. In diesem Fall öffnet jeder Worker eine Deskriptor zu lorem.txt, sucht zufällig darin und schreibt Zeilen nach /dev/null.
cmd: python run_benchmarks.py file_io.py
Importing file_io
Starting tests ...
non_threaded (1 iters) 0.057750 seconds
threaded (1 threads) 0.089992 seconds
processes (1 procs) 0.090817 seconds
non_threaded (2 iters) 0.180256 seconds
threaded (2 threads) 0.329961 seconds
processes (2 procs) 0.096683 seconds
non_threaded (4 iters) 0.370841 seconds
threaded (4 threads) 1.103678 seconds
processes (4 procs) 0.101535 seconds
non_threaded (8 iters) 0.749571 seconds
threaded (8 threads) 2.437204 seconds
processes (8 procs) 0.203438 seconds
Wie Sie sehen, ist pyprocessing bei dieser I/O-Operation immer noch schneller als die Verwendung mehrerer Threads. Und die Verwendung mehrerer Threads ist langsamer als die Single-Thread-Ausführung selbst.
Schließlich führen wir einen Socket-basierten Test durch, um die Netzwerk-I/O-Leistung zu zeigen. Diese Funktion holt eine URL von einem Server im LAN, der eine einfache Fehlerseite von Tomcat ist. Sie holt die Seite 100 Mal. Das Netzwerk ist ruhig und eine 10-Gbit/s-Verbindung.
cmd: python run_benchmarks.py url_get.py
Importing url_get
Starting tests ...
non_threaded (1 iters) 0.124774 seconds
threaded (1 threads) 0.120478 seconds
processes (1 procs) 0.121404 seconds
non_threaded (2 iters) 0.239574 seconds
threaded (2 threads) 0.146138 seconds
processes (2 procs) 0.138366 seconds
non_threaded (4 iters) 0.479159 seconds
threaded (4 threads) 0.200985 seconds
processes (4 procs) 0.188847 seconds
non_threaded (8 iters) 0.960621 seconds
threaded (8 threads) 0.659298 seconds
processes (8 procs) 0.298625 seconds
Wir sehen endlich, wie die Thread-Leistung die der Single-Thread-Ausführung übertrifft, aber das pyprocessing-Paket ist bei steigender Anzahl von Workern immer noch schneller. Wenn Sie bei einem oder zwei Threads/Workern bleiben, ist die Zeit zwischen Threads und pyprocessing ziemlich gleich.
Ein Punkt, der jedoch zu beachten ist, ist, dass es einen impliziten Overhead in der Queue-Implementierung des pyprocessing-Pakets aufgrund der Objektserialisierung gibt.
Alec Thomas stellte ein kurzes Beispiel zur Verfügung, das auf dem Skript run_benchmarks.py basiert, um diesen Overhead im Vergleich zur Standard-Queue-Implementierung zu demonstrieren.
cmd: run_bench_queue.py
non_threaded (1 iters) 0.010546 seconds
threaded (1 threads) 0.015164 seconds
processes (1 procs) 0.066167 seconds
non_threaded (2 iters) 0.020768 seconds
threaded (2 threads) 0.041635 seconds
processes (2 procs) 0.084270 seconds
non_threaded (4 iters) 0.041718 seconds
threaded (4 threads) 0.086394 seconds
processes (4 procs) 0.144176 seconds
non_threaded (8 iters) 0.083488 seconds
threaded (8 threads) 0.184254 seconds
processes (8 procs) 0.302999 seconds
Zusätzliche Benchmarks finden Sie im Verzeichnis examples/ der Quellverteilung des pyprocessing-Pakets. Die Beispiele werden in der Dokumentation des Pakets enthalten sein.
Wartung
Richard M. Oudkerk – der Autor des pyprocessing-Pakets hat zugestimmt, das Paket in Python SVN zu pflegen. Jesse Noller hat sich ebenfalls freiwillig gemeldet, das Paket mit zu pflegen/zu dokumentieren und zu testen.
API-Benennung
Während das Ziel der API des Pakets darin besteht, der des Threading- und Queue-Moduls unter Python 2.x genau zu ähneln, sind diese Module nicht PEP 8-konform. Es wurde entschieden, dass anstatt das Paket „wie es ist“ aufzunehmen und somit die Nicht-PEP-8-Konformität beizubehalten, alle APIs, Klassen usw. umbenannt werden, um vollständig PEP 8-konform zu sein.
Diese Änderung beeinträchtigt die einfache Austauschbarkeit für Benutzer des Threading-Moduls, aber das ist laut Ansicht der Autoren ein akzeptabler Nebeneffekt, insbesondere angesichts der Tatsache, dass die API des Threading-Moduls selbst geändert wird.
Issue 3042 im Tracker schlägt vor, dass es für Python 2.6 zwei APIs für das Threading-Modul geben wird – die aktuelle und die PEP 8-konforme. Warnungen vor der bevorstehenden Entfernung der ursprünglichen Java-Stil-API werden bei Aufruf von -3 ausgegeben.
In Python 3000 wird die Threading-API PEP 8-konform sein, was bedeutet, dass das multiprocessing-Modul und das threading-Modul wieder übereinstimmende APIs haben werden.
Zeitplan/Terminplanung
Einige Bedenken wurden hinsichtlich der Zeitplanung/Spätigkeit dieser PEP für die diesjährigen Veröffentlichungen 2.6 und 3.0 geäußert. Es wird jedoch von beiden Autoren und anderen als wichtig erachtet, dass die Funktionalität, die dieses Paket bietet, das Risiko der Aufnahme überwiegt.
Unter Berücksichtigung des Wunsches, Python-core nicht zu destabilisieren, kann jedoch eine gewisse Umgestaltung des pyprocessing-Codes „in“ Python-core bis zu den nächsten Veröffentlichungen 2.x/3.x zurückgestellt werden. Das bedeutet, dass das tatsächliche Risiko für Python-core minimal ist und sich hauptsächlich auf das Paket selbst beschränkt.
Offene Fragen
- Bestätigen Sie keine „standardmäßigen“ Remote-Verbindungsfunktionen; wenn nötig, aktivieren Sie die Remote-Sicherheitsmechanismen standardmäßig für die Klassen, die Remote-Funktionen bieten.
- Einige der API (
Queue-Methodenqsize(),task_done()undjoin()) müssen entweder hinzugefügt werden oder der Grund für ihren Ausschluss muss identifiziert und klar dokumentiert werden.
Geschlossene Probleme
- Der von roudkerk in Issue 1683 eingereichte
PyGILState-Bug-Patch muss angewendet werden, damit die Unit-Tests des Pakets funktionieren. - Vorhandene Dokumentation muss in ReST-Formatierung konvertiert werden.
- Abhängigkeit von ctypes: Die Abhängigkeit des
pyprocessing-Pakets von ctypes verhindert, dass das Paket auf Plattformen funktioniert, auf denen ctypes nicht unterstützt wird. Dies ist keine Einschränkung dieses Pakets, sondern von ctypes. - FERTIG: Umbenennen des Top-Level-Pakets von „pyprocessing“ in „multiprocessing“.
- FERTIG: Beachten Sie auch, dass das Standardverhalten des Prozess-Spawns es nicht mit der Verwendung in IDLE kompatibel macht, dies wird als Bugfix oder „setExecutable“-Verbesserung untersucht werden.
- FERTIG: Hinzufügen der Methode „multiprocessing.setExecutable()“, um das Standardverhalten des Pakets zu überschreiben, Prozesse mit dem aktuellen Ausführungsnamen anstelle des Python-Interpreters zu spawnen. Beachten Sie, dass Mark Hammond eine Factory-ähnliche Schnittstelle hierfür vorgeschlagen hat [7].
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0371.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT