Multiprocessing in Python - was wird vom Forkserver-Prozess vom übergeordneten Prozess geerbt?

Aug 15 2020

Ich versuche zu verwenden und bin in Worker-Prozessen forkserveraufgetreten .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

1 alex_noname Aug 16 2020 at 19:32

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:

  1. 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

  2. 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.

  3. 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
1 sgyzetrov Aug 17 2020 at 03:00

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_objecter nirgendwo in serverprocess und in all den Worker-Prozessen zu finden ist, die von dem serverprocess abzweigen .

Die Lösung von Alex funktioniert, da large_objectsie 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 forkserverwird 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 forkals 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_objecterhalten Sie mit direkt Zugriff auf large_objectin .worker_func

Dies forkserverist 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_objectan erster Stelle bringen, sind speicherintensiv, daher möchte ich keine unnötigen Ressourcen in meinen Worker-Prozessen.

Wenn ich all diese Berechnungen inherited.pywie 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.pyauch 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.pyzuerst 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 forkoder verrückt werden forkserver.