multiprocesamiento en python: ¿qué hereda el proceso forkserver del proceso principal?
Estoy tratando de usar forkserver
y me encontré NameError: name 'xxx' is not defined
en procesos de trabajo.
Estoy usando Python 3.6.4, pero la documentación debería ser la misma, desdehttps://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methodsdice que:
El proceso del servidor de bifurcación tiene un solo subproceso, por lo que es seguro usar os.fork(). No se heredan recursos innecesarios.
Además, dice:
Mejor heredar que encurtir/desencurtir
Al usar los métodos de inicio de spawn o forkserver , muchos tipos de multiprocesamiento deben ser seleccionables para que los procesos secundarios puedan usarlos . Sin embargo, por lo general se debe evitar enviar objetos compartidos a otros procesos mediante conductos o colas. En su lugar, debe organizar el programa de modo que un proceso que necesite acceso a un recurso compartido creado en otro lugar pueda heredarlo de un proceso antepasado.
Entonces, aparentemente, un objeto clave en el que mi proceso de trabajo necesita trabajar no fue heredado por el proceso del servidor y luego pasó a los trabajadores, ¿por qué sucedió eso? Me pregunto qué hereda exactamente el proceso forkserver del proceso principal.
Así es como se ve mi código:
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)
¡Gracias!
Mejor,
Respuestas
Teoría
A continuación se muestra un extracto del blog de Bojan Nikolic
Las versiones modernas de Python (en Linux) ofrecen tres formas de iniciar los procesos por separado:
Fork() -ing los procesos principales y continuando con la misma imagen de procesos tanto en el padre como en el hijo. Este método es rápido, pero potencialmente poco confiable cuando el estado principal es complejo
Generar los procesos secundarios, es decir, fork()-ing y luego execv para reemplazar la imagen del proceso con un nuevo proceso de Python. Este método es confiable pero lento, ya que la imagen del proceso se vuelve a cargar.
El mecanismo forkserver , que consiste en un servidor Python separado con un estado relativamente simple y que se bifurca cuando se necesita un nuevo proceso. Este método combina la velocidad de Fork()-ing con buena confiabilidad (porque el padre que se bifurca está en un estado simple).
bifurcador
El tercer método, forkserver , se ilustra a continuación. Tenga en cuenta que los niños conservan una copia del estado del forkserver. Se pretende que este estado sea relativamente simple, pero es posible ajustarlo a través de la API multiproceso a través del
set_forkserver_preload()
método.![]()
Práctica
Por lo tanto, si desea que los procesos secundarios hereden algo del padre, esto debe especificarse en el estado de forkserver por medio de set_forkserver_preload(modules_names)
, que establece una lista de nombres de módulos para intentar cargar en el proceso de forkserver. Doy un ejemplo a continuación:
# 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)
Producción:
# 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
Y si no usamos la precarga
...
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
Entonces, después de una discusión inspiradora con Alex, creo que tengo suficiente información para responder a mi pregunta: ¿qué hereda exactamente el proceso forkserver del proceso principal?
Básicamente, cuando se inicia el proceso del servidor, importará su módulo principal y if __name__ == '__main__'
se ejecutará todo lo anterior. Es por eso que mi código no funciona, porque large_object
no se encuentra en ninguna parte del server
proceso y en todos esos procesos de trabajo que se bifurcan del server
proceso .
La solución de Alex funciona porque large_object
ahora se importa tanto al proceso principal como al del servidor, por lo que todos los trabajadores bifurcados del servidor también obtendrán large_object
. Si se combina con set_forkserver_preload(modules_names)
todos los trabajadores, incluso podría obtener lo mismo large_object
por lo que vi. El motivo de uso forkserver
se explica explícitamente en la documentación de Python y en el blog de Bojan:
Cuando el programa se inicia y selecciona el método de inicio forkserver, se inicia un proceso de servidor. A partir de ese momento, cada vez que se necesita un nuevo proceso, el proceso principal se conecta al servidor y solicita que se bifurque un nuevo proceso. El proceso del servidor de bifurcación tiene un solo subproceso, por lo que es seguro usar os.fork(). No se heredan recursos innecesarios .
El mecanismo forkserver, que consiste en un servidor Python separado con un estado relativamente simple y que se bifurca cuando se necesita un nuevo proceso. Este método combina la velocidad de Fork()-ing con buena confiabilidad (porque el padre que se bifurca está en un estado simple) .
Entonces, aquí está más en el lado seguro de la preocupación.
Sin embargo, en una nota al margen, si lo usa fork
como método de inicio, no necesita importar nada, ya que todos los procesos secundarios obtienen una copia de la memoria del proceso principal (o una referencia si el sistema usa COW- copy-on-write
, corríjame si estoy equivocado). En este caso, usar global large_object
le dará acceso directamente.large_object
worker_func
Es forkserver
posible que no sea un enfoque adecuado para mí porque el problema al que me enfrento es la sobrecarga de memoria. Todas las operaciones que me large_object
ponen en primer lugar consumen memoria, por lo que no quiero recursos innecesarios en mis procesos de trabajo.
Si pongo todos esos cálculos directamente inherited.py
como sugirió Alex, se ejecutará dos veces (una cuando importé el módulo en main y otra cuando el servidor lo importe; ¿quizás incluso más cuando nacieron los procesos de trabajo?), esto es adecuado si solo quiere un proceso seguro de un solo subproceso del que los trabajadores puedan bifurcarse. Pero como estoy tratando de que los trabajadores no hereden recursos innecesarios y solo obtengan large_object
, esto no funcionará. Y poner esos cálculos tampoco funcionará ya que ahora ninguno de los procesos los ejecutará, incluidos el principal y el servidor __main__
.inherited.py
Entonces, como conclusión, si el objetivo aquí es lograr que los trabajadores hereden recursos mínimos, es mejor dividir mi código en 2, hacer calculation.py
primero, encurtir el large_object
, salir del intérprete y comenzar uno nuevo para cargar el encurtido large_object
. Entonces puedo volverme loco con fork
o forkserver
.