Estoy aprendiendo cómo utilizar el threading
y los multiprocessing
módulos en Python para ejecutar ciertas operaciones en paralelo y acelerar mi código.
Me resulta difícil (tal vez porque no tengo antecedentes teóricos al respecto) comprender cuál es la diferencia entre un threading.Thread()
objeto y multiprocessing.Process()
uno.
Además, no está del todo claro para mí cómo crear una instancia de una cola de trabajos y tener solo 4 (por ejemplo) ejecutándose en paralelo, mientras que el otro espera que los recursos se liberen antes de ejecutarse.
Los ejemplos en la documentación me parecen claros, pero no muy exhaustivos; Tan pronto como trato de complicar un poco las cosas, recibo muchos errores extraños (como un método que no se puede encurtir, etc.).
Entonces, ¿cuándo debo usar los módulos threading
y multiprocessing
?
¿Me puede vincular a algunos recursos que explican los conceptos detrás de estos dos módulos y cómo usarlos adecuadamente para tareas complejas?
Thread
módulo (llamado_thread
en python 3.x). Para ser honesto, nunca he entendido las diferencias mí mismo ...Thread
/_thread
dice explícitamente, son "primitivas de bajo nivel". Puede usarlo para construir objetos de sincronización personalizados, para controlar el orden de unión de un árbol de hilos, etc. Si no puede imaginar por qué necesitaría usarlo, no lo use y quédese con élthreading
.Respuestas:
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: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:
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 enmultiprocessing.Process
lugar de confiar en las piscinas. Y tendrá que crearqueue.Queue
umultiprocessing.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:
ctypes
tipos.threading
módulo no tiene algunas de las características delmultiprocessing
módulo. (Puede usarmultiprocessing.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 comoconcurrent.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.
fuente
pickle
documentos que explican), y luego en el peor de su envoltorio es estúpidadef wrapper(obj, *args): return obj.wrapper(*args)
.Pueden existir múltiples hilos en un solo proceso. Los hilos que pertenecen al mismo proceso comparten la misma área de memoria (pueden leer y escribir en las mismas variables, y pueden interferir entre sí). Por el contrario, diferentes procesos viven en diferentes áreas de memoria, y cada uno de ellos tiene sus propias variables. Para comunicarse, los procesos tienen que usar otros canales (archivos, tuberías o sockets).
Si desea paralelizar un cálculo, es probable que necesite varios subprocesos, porque probablemente desee que los subprocesos cooperen en la misma memoria.
Hablando sobre el rendimiento, los subprocesos son más rápidos de crear y administrar que los procesos (porque el sistema operativo no necesita asignar un área de memoria virtual completamente nueva), y la comunicación entre subprocesos suele ser más rápida que la comunicación entre procesos. Pero los hilos son más difíciles de programar. Los subprocesos pueden interferir entre sí y pueden escribirse en la memoria del otro, pero la forma en que esto sucede no siempre es obvia (debido a varios factores, principalmente el reordenamiento de la instrucción y el almacenamiento en memoria caché), por lo que necesitará primitivas de sincronización para controlar el acceso a tus variables.
fuente
threading
ymultiprocessing
.Creo que este enlace responde a su pregunta de una manera elegante.
Para ser breve, si uno de sus subproblemas tiene que esperar mientras otro termina, el subprocesamiento múltiple es bueno (en operaciones pesadas de E / S, por ejemplo); por el contrario, si sus subproblemas realmente pueden ocurrir al mismo tiempo, se sugiere el multiprocesamiento. Sin embargo, no creará más procesos que su número de núcleos.
fuente
Cotizaciones de documentación de Python
He destacado las citas clave de la documentación de Python sobre Process vs Threads y el GIL en: ¿Qué es el bloqueo global del intérprete (GIL) en CPython?
Experimentos de proceso vs hilo
Hice un poco de evaluación comparativa para mostrar la diferencia más concretamente.
En el punto de referencia, cronometré el trabajo de CPU e IO para varios números de subprocesos en una CPU de 8 hyperthread . El trabajo proporcionado por hilo es siempre el mismo, de modo que más hilos significa más trabajo total suministrado.
Los resultados fueron:
Trazar datos .
Conclusiones:
Para el trabajo con CPU, el multiprocesamiento siempre es más rápido, presumiblemente debido a la GIL
para IO trabajo encuadernado. ambos son exactamente la misma velocidad
los subprocesos solo se escalan hasta aproximadamente 4x en lugar del esperado 8x ya que estoy en una máquina de 8 hyperthread.
Compare eso con un trabajo en CPU C POSIX que alcanza la aceleración esperada de 8x: ¿Qué significan 'real', 'user' y 'sys' en la salida del tiempo (1)?
TODO: No sé la razón de esto, debe haber otras ineficiencias de Python que entran en juego.
Código de prueba:
GitHub upstream + código de trazado en el mismo directorio .
Probado en Ubuntu 18.10, Python 3.6.7, en una computadora portátil Lenovo ThinkPad P51 con CPU: CPU Intel Core i7-7820HQ (4 núcleos / 8 hilos), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD: Samsung MZVLB512HAJQ- 000L7 (3.000 MB / s).
Visualice qué hilos se están ejecutando en un momento dado
Esta publicación https://rohanvarma.me/GIL/ me enseñó que puede ejecutar una devolución de llamada cada vez que se programa un hilo con el
target=
argumento dethreading.Thread
y lo mismo paramultiprocessing.Process
.Esto nos permite ver exactamente qué hilo se ejecuta en cada momento. Cuando esto esté hecho, veríamos algo como (hice este gráfico en particular):
lo que demostraría que:
fuente
Aquí hay algunos datos de rendimiento para python 2.6.x que cuestionan la noción de que el subprocesamiento es más eficaz que el multiprocesamiento en escenarios vinculados a IO. Estos resultados provienen de un IBM System x3650 M4 BD de 40 procesadores.
Procesamiento IO-Bound: el grupo de procesos se desempeñó mejor que el grupo de subprocesos
Procesamiento vinculado a la CPU: el grupo de procesos se desempeñó mejor que el grupo de subprocesos
Estas no son pruebas rigurosas, pero me dicen que el multiprocesamiento no es totalmente improductivo en comparación con los subprocesos.
Código utilizado en la consola interactiva de Python para las pruebas anteriores
fuente
>>> do_work(50, 300, 'thread', 'fileio') --> 237.557 ms
>>> do_work(50, 300, 'process', 'fileio') --> 323.963 ms
>>> do_work(50, 2000, 'thread', 'square') --> 232.082 ms
>>> do_work(50, 2000, 'process', 'square') --> 282.785 ms
Bueno, la mayor parte de la pregunta es respondida por Giulio Franco. Seguiré explicando el problema del consumidor-productor, que supongo que lo pondrá en el camino correcto para su solución al uso de una aplicación multiproceso.
Puede leer más sobre las primitivas de sincronización en:
El pseudocódigo está arriba. Supongo que deberías buscar el problema productor-consumidor para obtener más referencias.
fuente