Lo que dice Giulio Franco es cierto para el subprocesamiento múltiple frente al multiprocesamiento en general .
Sin embargo, Python * tiene un problema adicional: hay un bloqueo global de intérprete que evita que dos hilos en el mismo proceso ejecuten código Python al mismo tiempo. Esto significa que si tiene 8 núcleos y cambia su código para usar 8 hilos, no podrá usar 800% de CPU y ejecutar 8x más rápido; utilizará la misma CPU al 100% y se ejecutará a la misma velocidad. (En realidad, funcionará un poco más lento, porque hay una sobrecarga adicional por el enhebrado, incluso si no tiene datos compartidos, pero ignore eso por ahora).
Existen excepciones para esto. Si el cálculo pesado de su código en realidad no ocurre en Python, pero en alguna biblioteca con código C personalizado que realiza el manejo adecuado de GIL, como una aplicación complicada, obtendrá el beneficio de rendimiento esperado de los subprocesos. Lo mismo es cierto si el cálculo pesado se realiza mediante algún subproceso que ejecuta y espera.
Más importante aún, hay casos en los que esto no importa. Por ejemplo, un servidor de red pasa la mayor parte de su tiempo leyendo paquetes fuera de la red, y una aplicación GUI pasa la mayor parte del tiempo esperando los eventos del usuario. Una razón para usar subprocesos en un servidor de red o aplicación GUI es permitirle realizar "tareas en segundo plano" de larga ejecución sin detener el hilo principal de continuar sirviendo paquetes de red o eventos GUI. Y eso funciona bien con hilos de Python. (En términos técnicos, esto significa que los subprocesos de Python le brindan concurrencia, a pesar de que no le brindan paralelismo central).
Pero si está escribiendo un programa vinculado a la CPU en Python puro, usar más hilos generalmente no es útil.
El uso de procesos separados no tiene tales problemas con el GIL, porque cada proceso tiene su propio GIL separado. Por supuesto, todavía tiene las mismas compensaciones entre subprocesos y procesos que en cualquier otro idioma: es más difícil y más costoso compartir datos entre procesos que entre subprocesos, puede ser costoso ejecutar una gran cantidad de procesos o crear y destruir con frecuencia, etc. Pero el GIL pesa mucho en el equilibrio hacia los procesos, de una manera que no es cierta para, por ejemplo, C o Java. Por lo tanto, se encontrará utilizando el multiprocesamiento con mucha más frecuencia en Python que en C o Java.
Mientras tanto, la filosofía de "baterías incluidas" de Python trae buenas noticias: es muy fácil escribir código que se puede alternar entre hilos y procesos con un cambio de una línea.
Si diseña su código en términos de "trabajos" autónomos que no comparten nada con otros trabajos (o el programa principal) excepto entrada y salida, puede usar la concurrent.futures
biblioteca para escribir su código alrededor de un grupo de subprocesos como este:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...
Incluso puede obtener los resultados de esos trabajos y pasarlos a otros trabajos, esperar cosas en orden de ejecución o de finalización, etc .; lea la sección sobre Future
objetos para más detalles.
Ahora, si resulta que su programa usa constantemente el 100% de la CPU, y agregar más subprocesos solo lo hace más lento, entonces se encuentra con el problema GIL, por lo que debe cambiar a los procesos. Todo lo que tienes que hacer es cambiar esa primera línea:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
La única advertencia real es que los argumentos de sus trabajos y los valores de retorno deben ser seleccionables (y no tomar demasiado tiempo o memoria para ser procesados) para ser utilizables en el proceso cruzado. Por lo general, esto no es un problema, pero a veces lo es.
Pero, ¿qué pasa si sus trabajos no pueden ser independientes? Si puede diseñar su código en términos de trabajos que pasan mensajes de uno a otro, todavía es bastante fácil. Puede que tenga que usar threading.Thread
o en multiprocessing.Process
lugar de confiar en las piscinas. Y tendrá que crear queue.Queue
u multiprocessing.Queue
objetos explícitamente. (Hay muchas otras opciones: tuberías, enchufes, archivos con bandadas, ... pero el punto es que debe hacer algo manualmente si la magia automática de un ejecutor es insuficiente).
Pero, ¿qué pasa si ni siquiera puede confiar en pasar mensajes? ¿Qué sucede si necesita dos trabajos para que ambos muten la misma estructura y vean los cambios de los demás? En ese caso, deberá realizar una sincronización manual (bloqueos, semáforos, condiciones, etc.) y, si desea utilizar procesos, objetos explícitos de memoria compartida para arrancar. Esto es cuando el subprocesamiento múltiple (o multiprocesamiento) se vuelve difícil. Si puedes evitarlo, genial; Si no puede, tendrá que leer más de lo que alguien puede poner en una respuesta SO.
A partir de un comentario, quería saber qué es diferente entre los hilos y los procesos en Python. Realmente, si lees la respuesta de Giulio Franco y la mía y todos nuestros enlaces, eso debería cubrir todo ... pero un resumen definitivamente sería útil, así que aquí va:
- Los hilos comparten datos por defecto; los procesos no lo hacen.
- Como consecuencia de (1), el envío de datos entre procesos generalmente requiere su preparación y eliminación. ** **
- Como otra consecuencia de (1), compartir datos directamente entre procesos generalmente requiere ponerlos en formatos de bajo nivel como Value, Array y
ctypes
tipos.
- Los procesos no están sujetos a la GIL.
- En algunas plataformas (principalmente Windows), los procesos son mucho más caros de crear y destruir.
- Existen algunas restricciones adicionales en los procesos, algunas de las cuales son diferentes en diferentes plataformas. Consulte las pautas de programación para más detalles.
- El
threading
módulo no tiene algunas de las características del multiprocessing
módulo. (Puede usar multiprocessing.dummy
para obtener la mayor parte de la API que falta en la parte superior de los hilos, o puede usar módulos de nivel superior como concurrent.futures
y no preocuparse por eso).
* No es realmente Python, el lenguaje, el que tiene este problema, sino CPython, la implementación "estándar" de ese lenguaje. Algunas otras implementaciones no tienen un GIL, como Jython.
** Si está utilizando el método de inicio fork para multiprocesamiento, que puede en la mayoría de las plataformas que no son de Windows, cada proceso secundario obtiene los recursos que tenía el padre cuando se inició el niño, que puede ser otra forma de pasar datos a los niños.
Thread
módulo (llamado_thread
en python 3.x). Para ser honesto, nunca he entendido las diferencias mí mismo ...