La memoria multiproceso inherentemente compartida ya no funciona en py

La memoria multiproceso inherentemente compartida ya no funciona en python 3.10 (procedente de 3.6)

Entiendo que hay una variedad de técnicas para compartir memoria y estructuras de datos entre procesos en python. Esta pregunta es específicamente sobre esta memoria inherentemente compartida en los scripts de python que existían en python 3.6 pero parece que ya no existen en 3.10. ¿Alguien sabe por qué y si es posible recuperar esto en 3.10? ¿O cuál es este cambio que estoy observando? Actualicé mi Mac a Monterey y ya no es compatible con Python 3.6, por lo que me veo obligado a actualizar a 3.9 o 3.10+.

Nota: Tiendo a desarrollar en Mac y ejecutar la producción en Ubuntu. No estoy seguro si eso influye aquí. Históricamente, con 3.6, todo se comportaba igual independientemente del sistema operativo.

Haga un proyecto simple con los siguientes archivos de python

miBiblioteca.py

MyDict = {}

prueba.py

import threading
import time
import multiprocessing

import myLibrary


def InitMyDict():
    myLibrary.MyDict = {'woot': 1, 'sauce': 2}
    print('initialized myLibrary.MyDict to ', myLibrary.MyDict)


def MainLoop():
    numOfSubProcessesToStart = 3
    for i in range(numOfSubProcessesToStart):
        t = threading.Thread(
            target=CoolFeature(),
            args=())
        t.start()

    while True:
        time.sleep(1)


def CoolFeature():
    MyProcess = multiprocessing.Process(
        target=SubProcessFunction,
        args=())
    MyProcess.start()


def SubProcessFunction():
    print('SubProcessFunction: ', myLibrary.MyDict)


if __name__ == '__main__':
    InitMyDict()
    MainLoop()

Cuando ejecuto esto en 3.6, tiene un comportamiento significativamente diferente al de 3.10. Entiendo que un subproceso no puede modificar la memoria del proceso principal, pero aun así es muy conveniente acceder a la estructura de datos del proceso principal que se configuró previamente en lugar de mover cada pequeña cosa a la memoria compartida solo para leer un simple diccionario/int/cadena/etc.

Salida de Python 3.10:

python3.10 test.py 
initialized myLibrary.MyDict to  {'woot': 1, 'sauce': 2}
SubProcessFunction:  {}
SubProcessFunction:  {}
SubProcessFunction:  {}

Salida de Python 3.6:

python3.6 test.py 
initialized myLibrary.MyDict to  {'woot': 1, 'sauce': 2}
SubProcessFunction:  {'woot': 1, 'sauce': 2}
SubProcessFunction:  {'woot': 1, 'sauce': 2}
SubProcessFunction:  {'woot': 1, 'sauce': 2}

Observación:

Observe que en 3.6, el subproceso puede ver el valor que se estableció desde el proceso principal. Pero en 3.10, el subproceso ve un diccionario vacío.

Mostrar la mejor respuesta

target=CoolFeature() no tiene sentido. ¿Por qué este caso de prueba necesita subprocesos en primer lugar?

@ o11c Puede ignorar el hilo. Este comportamiento todavía existe sin subprocesos. El caso de prueba no necesita subprocesos en absoluto. Estaba tratando de reproducir el error y estaba tratando de parecerse mucho a lo que estaba haciendo mi código.

No estoy particularmente familiarizado con multiprocessing, pero ¿es posible que esté utilizando un grupo basado en spawn en lugar de un grupo basado en fork?

Esto no es "memoria inherentemente compartida". multiprocessing nunca ha ofrecido tal función. ¿Estás en Mac? El método de inicio predeterminado cambió en 3.8 en Mac, lo que explicaría la diferencia observada.

@ o11c Esto puede ser acertado. Vea el enlace a continuación. "Cambiado en la versión 3.8: en macOS, el método de inicio de generación ahora es el predeterminado. El método de inicio de bifurcación debe considerarse inseguro ya que puede provocar fallas en el subproceso". Pero me pregunto qué significa esto para Ubuntu vs mac aquí... Ya que uso ambos. docs.python.org/3/library/…

@ user2357112supportsMonica Lea el enlace que comenté anteriormente, sobre el método de bifurcación: "Todos los recursos del padre son heredados por el proceso hijo". interesante No sabía que había tantas formas únicas de iniciar un proceso

@LampShade funciona forkserver? Recuerde que es peligroso llamar a fork en un proceso de subprocesos múltiples (a menos que pueda escribir con cuidado un código seguro para señales asíncronas y luego llamar inmediatamente a exec, que es IMPOSIBLE en Python y también es incompatible con lo que está haciendo de todos modos). Pero si puede hacer fork antes de crear cualquier hilo, debería funcionar bien.

"los recursos de los padres se heredan". Eso no significa "compartido". Un proceso bifurcado obtiene una copia (más probablemente una copia al escribir). Si un proceso cambia su copia, los otros procesos no ven el cambio.

@ o11c Mi proceso principal es de subprocesos múltiples. Así que parece que esto podría causar problemas. Esos problemas solo ocurren en una Mac, ¿verdad? Mis servidores de producción son todos Ubuntu. Pero, ¿qué quiere decir exactamente con "si puede hacer la bifurcación antes de crear cualquier hilo, debería funcionar bien". ¿Quieres decir si llamo a multiprocessing.set_start_method("fork") antes de iniciar cualquier hilo? ¿O quiere decir si genero mis subprocesos antes de iniciar cualquier hilo? En este momento, estoy iniciando subprocesos en el proceso principal antes de iniciar los subprocesos.

@MarkTolonen Sí, exactamente correcto. Eso es súper útil en muchos casos.

@LampShade Es un problema en todos los sistemas basados ​​en bifurcaciones: puede haber bloqueos que no se restablecen en la bifurcación. En Linux, GLIBC intenta restablecer los bloqueos si los conoce, pero los bloqueos en otras bibliotecas son inútiles. ... Me refiero a set_start_method("forkserver"). Como no estoy familiarizado con multiprocessing, es posible que podría tener que llamar (o tal vez simplemente llamar a una tarea ficticia); tenga en cuenta que esto solo funcionará si sus datos se calculan antes de la bifurcación (y antes de los subprocesos) ... Además: set_start_method("fork") NO funcionará en MacOS ya que la razón por la que se cambió el valor predeterminado fue porque comenzó a fallar de manera confiable.

En resumen, desde 3.8, CPython usa el método de inicio spawn en MacOs. Antes usaba el método fork.

En las plataformas UNIX, se utiliza el método de inicio fork, lo que significa que cada nuevo proceso multiprocessing es una copia exacta del padre en el momento de la bifurcación.

El método spawn significa que inicia un nuevo intérprete de Python para cada nuevo proceso multiprocessing. Según la documentación:

El proceso secundario solo heredará los recursos necesarios para ejecutar el método run() del objeto de proceso.

Importará su programa en este nuevo intérprete, por lo que iniciar procesos, etcétera, solo debe realizarse desde el bloque if __name__ == '__main__':.

Esto significa que no puede contar con que las variables del proceso padre estén disponibles en los hijos, a menos que sean constantes a nivel de módulo que se importarían.

Entonces el cambio es significativo.

¿Qué se puede hacer?

Si la información requerida pudiera ser una constante a nivel de módulo, eso resolvería el problema de la manera más simple.

Si eso no es posible (por ejemplo, porque los datos deben generarse en tiempo de ejecución), puede hacer que el padre escriba la información para compartirla en un archivo. P.ej. en formato JSON y antes de que inicie otros procesos. Entonces los niños podrían simplemente leer esto. Esa es probablemente la siguiente solución más simple.

Usar un multiprocessing.Manager le permitiría compartir un dict entre procesos. Sin embargo, hay una cierta cantidad de gastos generales asociados con esto.

O puede intentar llamar a multiprocessing.set_start_method("fork") antes de crear procesos o grupos y ver si no falla en su caso. Eso volvería al método anterior a 3.8 en MacOs. Pero como se documenta en este error, existen problemas reales con el uso del método fork en MacOs. Leer el problema indica que fork podría estar bien siempre y cuando no utilice hilos.

Gracias, esta publicación ayudó a resolver un error muy complicado que teníamos. Además, tener un comportamiento diferente en diferentes plataformas rompe el principio de menor sorpresa. Esto debería mencionarse más claramente en la documentación.

@UsmanIsmail El comportamiento diferente no es algo que los desarrolladores de Python puedan ayudar. Es sorprendente que hayan logrado que el multiprocesamiento funcione tan bien como lo hace sin fork. Y los métodos de inicio son mencionados en el manual. Aunque estoy de acuerdo en que podría ser más destacado.