multitraitement en python - qu'est-ce qui est hérité par le processus forkserver du processus parent?
J'essaie d'utiliser forkserver
et j'ai rencontré NameError: name 'xxx' is not defined
des processus de travail.
J'utilise Python 3.6.4, mais la documentation devrait être la même, à partir dehttps://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methodsil dit que:
Le processus du serveur fork est à thread unique, il est donc sûr d'utiliser os.fork(). Aucune ressource inutile n'est héritée.
Aussi, il dit :
Mieux vaut hériter que décaper/décaper
Lors de l'utilisation des méthodes de démarrage spawn ou forkserver , de nombreux types de multitraitement doivent être sélectionnables afin que les processus enfants puissent les utiliser . Cependant, il faut généralement éviter d'envoyer des objets partagés à d'autres processus à l'aide de canaux ou de files d'attente. Au lieu de cela, vous devez organiser le programme de sorte qu'un processus qui a besoin d'accéder à une ressource partagée créée ailleurs puisse l'hériter d'un processus ancêtre.
Donc, apparemment, un objet clé sur lequel mon processus de travail doit travailler n'a pas été hérité par le processus serveur puis transmis aux travailleurs, pourquoi cela s'est-il produit? Je me demande ce qui est exactement hérité par le processus forkserver du processus parent?
Voici à quoi ressemble mon code :
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)
Merci!
Meilleur,
Réponses
La théorie
Ci-dessous un extrait du blog de Bojan Nikolic
Les versions modernes de Python (sur Linux) proposent trois manières de démarrer les processus distincts :
Fork() -ing les processus parents et continue avec la même image de processus à la fois dans le parent et l'enfant. Cette méthode est rapide, mais potentiellement peu fiable lorsque l'état parent est complexe
Engendrant les processus enfants, c'est-à-dire, fork()-ing puis execv pour remplacer l'image de processus par un nouveau processus Python. Cette méthode est fiable mais lente, car l'image du processus est rechargée à nouveau.
Le mécanisme forkserver , qui consiste en un serveur Python séparé avec qui a un état relativement simple et qui est fork()-ed lorsqu'un nouveau processus est nécessaire. Cette méthode combine la vitesse de Fork()-ing avec une bonne fiabilité (car le parent en cours de forkage est dans un état simple).
Serveur de fourche
La troisième méthode, forkserver , est illustrée ci-dessous. Notez que les enfants conservent une copie de l'état de forkserver. Cet état est censé être relativement simple, mais il est possible de l'ajuster via l'API multiprocessus via la
set_forkserver_preload()
méthode.
Pratique
Ainsi, si vous voulez que quelque chose soit hérité par les processus enfants du parent, cela doit être spécifié dans l' état forkserver au moyen de set_forkserver_preload(modules_names)
, qui définit la liste des noms de modules à essayer de charger dans le processus forkserver. Je donne un exemple ci-dessous :
# 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)
Production:
# 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
Et si nous n'utilisons pas le préchargement
...
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
Donc, après une discussion inspirante avec Alex, je pense avoir suffisamment d'informations pour répondre à ma question : qu'est-ce qui est exactement hérité par le processus forkserver du processus parent ?
Fondamentalement, lorsque le processus serveur démarre, il importe votre module principal et tout ce qui précède if __name__ == '__main__'
sera exécuté. C'est pourquoi mon code ne fonctionne pas, car large_object
il est introuvable dans server
process et dans tous ces processus de travail qui dérivent du server
process .
La solution d'Alex fonctionne car large_object
elle est maintenant importée à la fois dans le processus principal et dans le processus du serveur, de sorte que chaque travailleur dérivé du serveur obtiendra également large_object
. S'il est combiné avec set_forkserver_preload(modules_names)
tous les travailleurs, il pourrait même obtenir le même résultat large_object
d'après ce que j'ai vu. La raison de l'utilisation forkserver
est explicitement expliquée dans les documentations Python et dans le blog de Bojan :
Lorsque le programme démarre et sélectionne la méthode de démarrage forkserver, un processus serveur est lancé. Dès lors, chaque fois qu'un nouveau processus est nécessaire, le processus parent se connecte au serveur et demande qu'il fork un nouveau processus. Le processus du serveur fork est à thread unique, il est donc sûr d'utiliser os.fork(). Aucune ressource inutile n'est héritée .
Le mécanisme forkserver, qui consiste en un serveur Python séparé avec qui a un état relativement simple et qui est fork()-ed lorsqu'un nouveau processus est nécessaire. Cette méthode combine la vitesse de Fork()-ing avec une bonne fiabilité (car le parent en cours de forkage est dans un état simple) .
C'est donc plus prudent ici.
En passant, si vous utilisez fork
comme méthode de démarrage, vous n'avez pas besoin d'importer quoi que ce soit puisque tous les processus enfants reçoivent une copie de la mémoire des processus parents (ou une référence si le système utilise COW- copy-on-write
, corrigez-moi si je suis mauvais). Dans ce cas, l'utilisation global large_object
vous permettra d'accéder directement à large_object
in .worker_func
Ce forkserver
n'est peut-être pas une approche appropriée pour moi car le problème auquel je suis confronté est la surcharge de mémoire. Toutes les opérations qui me large_object
concernent en premier lieu consomment de la mémoire, donc je ne veux pas de ressources inutiles dans mes processus de travail.
Si je mets tous ces calculs directement dans inherited.py
comme Alex l'a suggéré, il sera exécuté deux fois (une fois lorsque j'ai importé le module dans main et une fois lorsque le serveur l'importe; peut-être encore plus lorsque les processus de travail sont nés?), cela convient si je Je veux juste un processus sécurisé à un seul thread à partir duquel les travailleurs peuvent bifurquer. Mais comme j'essaie de faire en sorte que les travailleurs n'héritent pas de ressources inutiles et n'obtiennent que large_object
, cela ne fonctionnera pas. Et mettre ces calculs dans __main__
ne inherited.py
fonctionnera pas non plus puisque maintenant aucun des processus ne les exécutera, y compris le serveur principal et le serveur.
Donc, en conclusion, si le but ici est de faire en sorte que les travailleurs héritent d'un minimum de ressources, je ferais mieux de diviser mon code en 2, faites d' calculation.py
abord, pickle le large_object
, quittez l'interpréteur et en démarrez un nouveau pour charger le pickled large_object
. Ensuite, je peux juste devenir fou avec soit fork
ou forkserver
.