¿Cuáles son las diferencias entre los módulos de subprocesamiento y multiprocesamiento?

Estoy aprendiendo a usar los módulos de threading y multiprocessing en Python para ejecutar ciertas operaciones en paralelo y acelerar mi código.

Encuentro esto difícil (tal vez porque no tengo antecedentes teóricos al respecto) para entender cuál es la diferencia entre un objeto threading.Thread() y uno multiprocessing.Process() .

Además, no me queda del todo claro cómo crear una cola de trabajos y tener solo 4 (por ejemplo) de ellos ejecutándose en paralelo, mientras que el otro espera que los recursos se liberen antes de ejecutarse.

Encuentro los ejemplos en la documentación 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 puede ser decapado, etc.).

Entonces, ¿cuándo debo usar los módulos de threading y multiprocessing ?

    ¿Puede vincularme a algunos recursos que expliquen los conceptos detrás de estos dos módulos y cómo usarlos adecuadamente para tareas complejas?

    Lo que dice Giulio Franco es cierto para multiproceso frente a multiprocesamiento en general .

    Sin embargo, Python * tiene un problema adicional: hay un locking global de intérpretes que evita que dos subprocesos en el mismo proceso ejecuten el código de Python al mismo tiempo. Esto significa que si tiene 8 núcleos y cambia su código para usar 8 subprocesos, no podrá usar un 800% de CPU y correr 8 veces más rápido; Utilizará la misma CPU al 100% y se ejecutará a la misma velocidad. (En realidad, se ejecutará un poco más lento, porque la sobrecarga adicional de los hilos, incluso si no tienes datos compartidos, pero ignora eso por ahora).

    Existen excepciones para esto. Si los cálculos pesados ​​de su código no ocurren realmente en Python, pero en algunas bibliotecas con código C personalizado que realiza el manejo adecuado de GIL, como una aplicación numpy, obtendrá el beneficio de rendimiento esperado de los subprocesos. Lo mismo es cierto si el cálculo pesado se realiza mediante algún subproceso en el que se 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 de la red, y una aplicación GUI pasa la mayor parte de su tiempo esperando eventos del usuario. Una de las razones para usar subprocesos en un servidor de red o aplicación de GUI es permitirle realizar “tareas en segundo plano” de larga ejecución sin que el hilo principal continúe con el servicio de paquetes de red o eventos de GUI. Y eso funciona bien con hilos de Python. (En términos técnicos, esto significa que los hilos de Python te dan concurrencia, aunque no te dan un paralelismo de núcleo).

    Pero si estás escribiendo un progtwig vinculado a la CPU en Python puro, usar más subprocesos generalmente no es útil.

    El uso de procesos separados no tiene tales problemas con la GIL, porque cada proceso tiene su propia GIL separada. Por supuesto, todavía tiene las mismas ventajas y desventajas entre los 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, digamos, C o Java. Por lo tanto, se encontrará usando multiprocesamiento con mayor 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 puede alternar entre hilos y procesos con un cambio de una sola línea.

    Si diseña su código en términos de “trabajos” independientes que no comparten nada con otros trabajos (o el progtwig principal), excepto entrada y salida, puede usar la biblioteca concurrent.futures para escribir su código en torno a un grupo de subprocesos. Me gusta esto:

     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 las cosas en orden de ejecución o en orden de finalización, etc .; lea la sección de Objetos Future para más detalles.

    Ahora, si resulta que su progtwig está usando el 100% de la CPU y agregar más subprocesos solo lo hace más lento, entonces se está encontrando en el problema de GIL, por lo que necesita 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 los trabajos y los valores de retorno deben ser seleccionables (y no requieren demasiado tiempo o memoria para ser procesados) para poder ser procesados ​​en forma cruzada. Normalmente esto no es un problema, pero a veces lo es.


    Pero, ¿qué pasa si sus trabajos no pueden ser autónomos? 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. Es posible que tenga que usar threading.Thread o threading.Thread multiprocessing.Process lugar de depender de grupos. Y tendrá que crear queue.Queue multiprocessing.Queue objetos queue.Queue o multiprocessing.Queue . (Hay muchas otras opciones: tuberías, sockets, archivos con bandadas, … pero el punto es que debe hacer algo manualmente si la magia automática de un Ejecutor es insuficiente).

    Pero, ¿y si no puedes confiar en el paso de mensajes? ¿Qué sucede si necesita dos trabajos para mutar la misma estructura y ver los cambios de cada uno? En ese caso, deberá realizar una sincronización manual (lockings, semáforos, condiciones, etc.) y, si desea utilizar procesos, explícitamente objetos de memoria compartida para iniciar. Esto es cuando el subprocesamiento múltiple (o multiprocesamiento) se vuelve difícil. Si puedes evitarlo, genial; Si no puede, necesitará leer más de lo que alguien puede poner en una respuesta de SO.


    A partir de un comentario, quería saber qué diferencia hay entre los procesos y los subprocesos 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:

    1. Los hilos comparten datos por defecto; los procesos no lo hacen.
    2. Como consecuencia de (1), el envío de datos entre procesos generalmente requiere que se decapen y se eliminen. **
    3. Como otra consecuencia de (1), compartir datos directamente entre procesos generalmente requiere colocarlos en formatos de bajo nivel como los tipos Value, Array y ctypes .
    4. Los procesos no están sujetos a la GIL.
    5. En algunas plataformas (principalmente Windows), los procesos son mucho más caros de crear y destruir.
    6. Existen algunas restricciones adicionales en los procesos, algunas de las cuales son diferentes en diferentes plataformas. Vea las pautas de progtwigción para más detalles.
    7. El módulo de threading no tiene algunas de las características del módulo de multiprocessing . (Puede usar multiprocessing.dummy para obtener la mayor parte de la API faltante en la parte superior de los subprocesos, o puede usar módulos de nivel superior como concurrent.futures y no preocuparse por eso).

    * En realidad no es Python, el lenguaje, 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 de bifurcación para el multiprocesamiento, que puede hacer en la mayoría de las plataformas que no son de Windows, cada proceso secundario obtiene todos los recursos que tenía el padre cuando se inició el niño, lo que puede ser otra manera de pasar los datos a los niños.

    Múltiples hilos pueden existir 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 utilizar otros canales (archivos, tuberías o sockets).

    Si desea paralelizar un cálculo, probablemente necesitará multihilo, porque probablemente quiera que los hilos cooperen en la misma memoria.

    Hablando de 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 progtwigr. Los hilos pueden interferir entre sí y 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 instrucciones y el almacenamiento en memoria caché de la memoria), por lo que necesitará primitivas de sincronización para controlar el acceso. a sus variables.

    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 finaliza, el subprocesamiento múltiple es bueno (en operaciones pesadas de E / S, por ejemplo); por el contrario, si sus problemas secundarios realmente pueden suceder al mismo tiempo, se sugiere el multiprocesamiento. Sin embargo, no creará más procesos que su número de núcleos.

    Aquí hay algunos datos de rendimiento para Python 2.6.x que llaman para cuestionar 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 de IO-Bound: la agrupación de procesos funcionó mejor que la agrupación de hebras

     >>> do_work(50, 300, 'thread','fileio') do_work function took 455.752 ms >>> do_work(50, 300, 'process','fileio') do_work function took 319.279 ms 

    Procesamiento vinculado a la CPU: el grupo de procesos se desempeñó mejor que el grupo de subprocesos

     >>> do_work(50, 2000, 'thread','square') do_work function took 338.309 ms >>> do_work(50, 2000, 'process','square') do_work function took 287.488 ms 

    Estas no son pruebas rigurosas, pero me dicen que el multiprocesamiento no es completamente desfavorable en comparación con el subprocesamiento.

    Código utilizado en la consola interactiva de Python para las pruebas anteriores

     from multiprocessing import Pool from multiprocessing.pool import ThreadPool import time import sys import os from glob import glob text_for_test = str(range(1,100000)) def fileio(i): try : os.remove(glob('./test/test-*')) except : pass f=open('./test/test-'+str(i),'a') f.write(text_for_test) f.close() f=open('./test/test-'+str(i),'r') text = f.read() f.close() def square(i): return i*i def timing(f): def wrap(*args): time1 = time.time() ret = f(*args) time2 = time.time() print '%s function took %0.3f ms' % (f.func_name, (time2-time1)*1000.0) return ret return wrap result = None @timing def do_work(process_count, items, process_type, method) : pool = None if process_type == 'process' : pool = Pool(processes=process_count) else : pool = ThreadPool(processes=process_count) if method == 'square' : multiple_results = [pool.apply_async(square,(a,)) for a in range(1,items)] result = [res.get() for res in multiple_results] else : multiple_results = [pool.apply_async(fileio,(a,)) for a in range(1,items)] result = [res.get() for res in multiple_results] do_work(50, 300, 'thread','fileio') do_work(50, 300, 'process','fileio') do_work(50, 2000, 'thread','square') do_work(50, 2000, 'process','square') 

    Bueno, la mayor parte de la pregunta es respondida por Giulio Franco. Voy a profundizar más en el problema del consumidor-productor, que supongo que lo pondrá en el camino correcto para su solución al utilizar una aplicación de multiproceso.

     fill_count = Semaphore(0) # items produced empty_count = Semaphore(BUFFER_SIZE) # remaining space buffer = Buffer() def producer(fill_count, empty_count, buffer): while True: item = produceItem() empty_count.down(); buffer.push(item) fill_count.up() def consumer(fill_count, empty_count, buffer): while True: fill_count.down() item = buffer.pop() empty_count.up() consume_item(item) 

    Puedes leer más sobre las primitivas de sincronización de:

      http://linux.die.net/man/7/sem_overview http://docs.python.org/2/library/threading.html 

    El pseudocódigo está arriba. Supongo que debería buscar el problema productor-consumidor para obtener más referencias.