threads thread starmap set_start_method programming parallel example python multithreading parallel-processing multiprocessing

python - thread - ¿Cuáles son las diferencias entre los módulos de subprocesamiento y multiproceso?



starmap python multiprocessing (5)

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

Estoy encontrando esto difícil (tal vez porque no tengo ningún trasfondo teórico al respecto) para entender cuál es la diferencia entre un objeto threading.Thread() y un multiprocessing.Process() one.

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 a que los recursos se liberen antes de ejecutarse.

Encuentro claros los ejemplos en la documentación, pero no son 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 escalar, etc.).

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

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


Aquí hay algunos datos de rendimiento para python 2.6.x que llama a 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 IO-Bound: Process Pool se desempeñó mejor que Thread Pool

>>> 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 de 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 del todo insuficiente en comparación con el enhebrado.

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. Me extenderé más sobre el problema del productor consumidor, que supongo que lo pondrá en el camino correcto para su solución al uso de una aplicación 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)

Puede 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ías buscar el problema productor-consumidor para obtener más referencias.


Creo que este enlace responde a su pregunta de una manera elegante.

Para ser breve, si uno de sus sub-problemas tiene que esperar mientras otro finaliza, el multihilo es bueno (en operaciones pesadas de E / S, por ejemplo); por el contrario, si sus sub-problemas realmente podrían suceder al mismo tiempo, se sugiere el multiprocesamiento. Sin embargo, no creará más procesos que su número de núcleos.


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 usar otros canales (archivos, tuberías o zócalos).

Si quiere paralelizar un cálculo, probablemente necesite un 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 programar. Los hilos pueden interferir entre sí y pueden escribir 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


Lo que dice Giulio Franco es cierto para el multiprocesamiento frente al multiprocesamiento en general .

Sin embargo, Python * tiene un problema adicional: hay un bloqueo de intérprete global 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 hilos, no podrá usar 800% de CPU y ejecutar 8 veces más rápido; usará la misma CPU al 100% y funcionará a la misma velocidad. (En realidad, se ejecutará un poco más lento, debido a que hay una sobrecarga adicional debido al subprocesamiento, incluso si no tiene ningún dato compartido, pero ignórelo por el momento).

Existen excepciones para esto. Si el cómputo pesado de su código no ocurre realmente en Python, pero en alguna biblioteca con código C personalizado que maneje correctamente GIL, como una aplicación numpy, obtendrá los beneficios de rendimiento esperados de enhebrar. Lo mismo es cierto si el cálculo pesado lo realiza un subproceso que ejecuta y espera.

Más importante aún, hay casos en que esto no importa. Por ejemplo, un servidor de red pasa la mayor parte del tiempo leyendo paquetes fuera de la red, y una aplicación GUI pasa la mayor parte del tiempo esperando eventos del usuario. Una razón para utilizar 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 atendiendo paquetes de red o eventos GUI. Y eso funciona bien con los hilos de Python. (En términos técnicos, esto significa que los subprocesos de Python le otorgan concurrencia, aunque no le proporcionen core-parallelism).

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

El uso de procesos separados no presenta tales problemas con el GIL, ya que cada proceso tiene su propio GIL por separado. Por supuesto, todavía tiene las mismas compensaciones entre hilos y procesos que en cualquier otro idioma: es más difícil y más costoso compartir datos entre procesos que entre hilos, puede ser costoso ejecutar una gran cantidad de procesos o crear y destruir con frecuencia, etc. Pero el GIL pesa mucho en la balanza hacia los procesos, de una manera que no es cierta para, digamos, C o Java. Por lo tanto, te encontrarás usando multiprocesamiento mucho más a menudo en Python de lo que lo harías en C o Java.

Mientras tanto, la filosofía de "baterías incluidas" de Python trae algunas buenas noticias: es muy fácil escribir código que se puede cambiar entre procesos y subprocesos con un cambio de línea única.

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 los de entrada y salida, puede usar la biblioteca concurrent.futures para escribir su código en 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 sobre objetos Future para más detalles.

Ahora, si resulta que su programa usa constantemente 100% de CPU, y agregar más hilos solo lo hace más lento, entonces se encontrará con el problema de 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 y los valores de retorno de sus trabajos deben ser seleccionables (y no tomar demasiado tiempo o memoria para ser procesados) para que sean utilizables en el proceso cruzado. Por lo general, esto no es un problema, pero a veces lo es.

¿Pero qué pasa si tus 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. Puede que tenga que usar threading.Thread o multiprocessing.Process lugar de confiar en pools. Y tendrá que crear objetos queue.Queue o multiprocessing.Queue explícitamente. (Hay muchas otras opciones: tubos, tomas de corriente, archivos con bandadas, ... pero el punto es que tienes que hacer algo manualmente si la magia automática de un Ejecutor es insuficiente).

Pero, ¿y si ni siquiera puedes confiar en el envío de mensajes? ¿Qué pasa si necesita dos trabajos para mutar la misma estructura y ver los cambios de los demás? En ese caso, necesitará hacer una sincronización manual (bloqueos, semáforos, condiciones, etc.) y, si desea usar procesos, objetos explícitos de memoria compartida para arrancar. Esto es cuando el multihilo (o el 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 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:

  1. Los hilos comparten datos por defecto; los procesos no.
  2. Como consecuencia de (1), el envío de datos entre procesos generalmente requiere decapado y desmantelamiento. **
  3. Como otra consecuencia de (1), el intercambio directo de datos entre procesos generalmente requiere ponerlo en formatos de bajo nivel como Value, Array y ctypes .
  4. Los procesos no están sujetos a GIL.
  5. En algunas plataformas (principalmente Windows), los procesos son mucho más costosos de crear y destruir.
  6. Existen algunas restricciones adicionales en los procesos, algunas de las cuales son diferentes en diferentes plataformas. Ver las pautas de programació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 que falta sobre 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 usa el método fork start para multiprocesamiento, que puede usar en la mayoría de las plataformas que no son de Windows, cada proceso secundario obtiene los recursos que el padre tenía cuando se inició el niño, lo que puede ser otra forma de pasar datos a los niños.