Multiprocessing in Python - was wird vom Forkserver-Prozess vom übergeordneten Prozess geerbt?
Ich versuche zu verwenden und bin in Worker-Prozessen forkserver
aufgetreten .NameError: name 'xxx' is not defined
Ich verwende Python 3.6.4, aber die Dokumentation sollte die gleiche sein, vonhttps://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methodses steht dass:
Der Fork-Serverprozess ist Single-Threaded, daher ist es sicher, os.fork() zu verwenden. Es werden keine unnötigen Ressourcen vererbt.
Außerdem heißt es:
Besser erben als pickle/unpickle
Bei Verwendung der Spawn- oder Forkserver- Startmethoden müssen viele Typen aus dem Multiprocessing wählbar sein, damit untergeordnete Prozesse sie verwenden können . Es sollte jedoch generell vermieden werden, Shared Objects über Pipes oder Queues an andere Prozesse zu senden. Stattdessen sollten Sie das Programm so einrichten, dass ein Prozess, der Zugriff auf eine an anderer Stelle erstellte gemeinsam genutzte Ressource benötigt, diese von einem Vorgängerprozess erben kann.
Anscheinend wurde also ein Schlüsselobjekt, an dem mein Arbeitsprozess arbeiten muss, nicht vom Serverprozess geerbt und dann an die Arbeiter weitergegeben. Warum ist das passiert? Ich frage mich, was genau der Forkserver-Prozess vom übergeordneten Prozess erbt?
So sieht mein Code aus:
import multiprocessing
import (a bunch of other modules)
def worker_func(nameList):
global largeObject
for item in nameList:
# get some info from largeObject using item as index
# do some calculation
return [item, info]
if __name__ == '__main__':
result = []
largeObject # This is my large object, it's read-only and no modification will be made to it.
nameList # Here is a list variable that I will need to get info for each item in it from the largeObject
ctx_in_main = multiprocessing.get_context('forkserver')
print('Start parallel, using forking/spawning/?:', ctx_in_main.get_context())
cores = ctx_in_main.cpu_count()
with ctx_in_main.Pool(processes=4) as pool:
for x in pool.imap_unordered(worker_func, nameList):
result.append(x)
Vielen Dank!
Am besten,
Antworten
Theorie
Unten ist ein Auszug aus dem Blog von Bojan Nikolic
Moderne Python-Versionen (unter Linux) bieten drei Möglichkeiten, die einzelnen Prozesse zu starten:
Fork() -ing die übergeordneten Prozesse und fährt mit dem gleichen Prozessbild sowohl im übergeordneten als auch im untergeordneten Prozess fort. Diese Methode ist schnell, aber möglicherweise unzuverlässig, wenn der übergeordnete Zustand komplex ist
Spawnen der untergeordneten Prozesse, dh fork()-ing und dann execv, um das Prozessabbild durch einen neuen Python-Prozess zu ersetzen. Diese Methode ist zuverlässig, aber langsam, da das Prozessabbild neu geladen wird.
Der Forkserver- Mechanismus, der aus einem separaten Python-Server besteht, hat einen relativ einfachen Zustand und wird mit einer Gabelung (fork()) versehen, wenn ein neuer Prozess benötigt wird. Diese Methode kombiniert die Geschwindigkeit von Fork()-ing mit guter Zuverlässigkeit (da sich der Elternteil, der gegabelt wird, in einem einfachen Zustand befindet).
Forkserver
Die dritte Methode, forkserver , ist unten dargestellt. Beachten Sie, dass Kinder eine Kopie des Forkserver-Status behalten. Dieser Zustand soll relativ einfach sein, aber es ist möglich, dies durch die Multiprozess-API durch die
set_forkserver_preload()
Methode anzupassen.
Trainieren
Wenn Sie also möchten, dass etwas von untergeordneten Prozessen vom übergeordneten Prozess geerbt wird, muss dies im Forkserver -Status mit angegeben werden set_forkserver_preload(modules_names)
, wodurch eine Liste von Modulnamen festgelegt wird, die versucht werden, in den Forkserver-Prozess zu laden. Ich gebe unten ein Beispiel:
# inherited.py
large_obj = {"one": 1, "two": 2, "three": 3}
# main.py
import multiprocessing
import os
from time import sleep
from inherited import large_obj
def worker_func(key: str):
print(os.getpid(), id(large_obj))
sleep(1)
return large_obj[key]
if __name__ == '__main__':
result = []
ctx_in_main = multiprocessing.get_context('forkserver')
ctx_in_main.set_forkserver_preload(['inherited'])
cores = ctx_in_main.cpu_count()
with ctx_in_main.Pool(processes=cores) as pool:
for x in pool.imap(worker_func, ["one", "two", "three"]):
result.append(x)
for res in result:
print(res)
Ausgabe:
# The PIDs are different but the address is always the same
PID=18603, obj id=139913466185024
PID=18604, obj id=139913466185024
PID=18605, obj id=139913466185024
Und wenn wir das Vorladen nicht verwenden
...
ctx_in_main = multiprocessing.get_context('forkserver')
# ctx_in_main.set_forkserver_preload(['inherited'])
cores = ctx_in_main.cpu_count()
...
# The PIDs are different, the addresses are different too
# (but sometimes they can coincide)
PID=19046, obj id=140011789067776
PID=19047, obj id=140011789030976
PID=19048, obj id=140011789030912
Nach einer inspirierenden Diskussion mit Alex denke ich, dass ich genügend Informationen habe, um meine Frage zu beantworten: Was genau wird vom Forkserver-Prozess vom übergeordneten Prozess geerbt?
Grundsätzlich importiert der Serverprozess beim Start Ihr Hauptmodul und alles davor if __name__ == '__main__'
wird ausgeführt. Aus diesem Grund funktioniert mein Code nicht, da large_object
er nirgendwo in server
process und in all den Worker-Prozessen zu finden ist, die von dem server
process abzweigen .
Die Lösung von Alex funktioniert, da large_object
sie jetzt sowohl in den Haupt- als auch in den Serverprozess importiert wird, sodass jeder Worker, der vom Server gegabelt wird, auch large_object
. In Kombination mit set_forkserver_preload(modules_names)
allen Arbeitern könnte sogar das Gleiche large_object
herauskommen, was ich gesehen habe. Der Grund für die Verwendung forkserver
wird in Python-Dokumentationen und in Bojans Blog explizit erklärt:
Wenn das Programm startet und die Forkserver-Startmethode auswählt, wird ein Serverprozess gestartet. Von da an verbindet sich der übergeordnete Prozess immer dann mit dem Server, wenn ein neuer Prozess benötigt wird, und fordert an, dass er einen neuen Prozess forkt. Der Fork-Serverprozess ist Single-Threaded, daher ist es sicher, os.fork() zu verwenden. Es werden keine unnötigen Ressourcen vererbt .
Der Forkserver-Mechanismus, der aus einem separaten Python-Server besteht, hat einen relativ einfachen Zustand und wird mit einer Gabelung (fork()) versehen, wenn ein neuer Prozess benötigt wird. Diese Methode kombiniert die Geschwindigkeit von Fork()-ing mit guter Zuverlässigkeit (da sich der Elternteil, der gegabelt wird, in einem einfachen Zustand befindet) .
Hier ist man also eher auf der sicheren Seite.
Nebenbei bemerkt, wenn Sie fork
als Startmethode verwenden, müssen Sie nichts importieren, da alle untergeordneten Prozesse eine Kopie des übergeordneten Prozessspeichers erhalten (oder eine Referenz, wenn das System COW- verwendet copy-on-write
, bitte korrigieren Sie mich, wenn ich es bin falsch). In diesem Fall global large_object
erhalten Sie mit direkt Zugriff auf large_object
in .worker_func
Dies forkserver
ist möglicherweise kein geeigneter Ansatz für mich, da das Problem, mit dem ich konfrontiert bin, der Speicher-Overhead ist. Alle Operationen, die mich large_object
an erster Stelle bringen, sind speicherintensiv, daher möchte ich keine unnötigen Ressourcen in meinen Worker-Prozessen.
Wenn ich all diese Berechnungen inherited.py
wie von Alex vorgeschlagen direkt einfüge, werden sie zweimal ausgeführt (einmal, wenn ich das Modul in main importiert habe und einmal, wenn der Server es importiert; vielleicht sogar noch mehr, wenn Worker-Prozesse geboren wurden?), Dies ist geeignet, wenn ich wollen nur einen sicheren Single-Thread-Prozess, von dem Arbeiter forken können. Aber da ich versuche, Arbeiter dazu zu bringen, keine unnötigen Ressourcen zu erben und nur zu bekommen large_object
, wird das nicht funktionieren. Und das Einfügen dieser Berechnungen __main__
wird inherited.py
auch nicht funktionieren, da jetzt keiner der Prozesse sie ausführen wird, einschließlich main und server.
Wenn das Ziel hier also darin besteht, Arbeiter dazu zu bringen, minimale Ressourcen zu erben, bin ich besser dran, meinen Code in 2 aufzuteilen, calculation.py
zuerst zu tun, pickle die large_object
, beende den Interpreter und starte eine neue, um die eingelegte zu laden large_object
. Dann kann ich einfach mit entweder fork
oder verrückt werden forkserver
.