Multiprocesamiento vs Threading Python

Estoy tratando de entender las ventajas de multiprocesamiento sobre subprocesos . Sé que el multiprocesamiento evita el locking global de intérpretes, pero ¿qué otras ventajas existen y los subprocesos no pueden hacer lo mismo?

El módulo de threading utiliza subprocesos, el módulo de multiprocessing utiliza procesos. La diferencia es que los subprocesos se ejecutan en el mismo espacio de memoria, mientras que los procesos tienen memoria separada. Esto hace que sea un poco más difícil compartir objetos entre procesos con multiprocesamiento. Como los hilos usan la misma memoria, deben tomarse precauciones o dos hilos se escribirán en la misma memoria al mismo tiempo. Para esto está el locking global del intérprete.

Los procesos de desove son un poco más lentos que los hilos de desove. Una vez que están corriendo, no hay mucha diferencia.

Aquí hay algunos pros / contras que se me ocurrieron.

Multiprocesamiento

Pros

  • Espacio de memoria separado
  • El código es generalmente sencillo
  • Aprovecha múltiples CPUs y núcleos
  • Evita las limitaciones de GIL para cPython
  • Elimina la mayoría de las necesidades de primitivas de sincronización a menos que use una memoria compartida (en su lugar, es más bien un modelo de comunicación para IPC)
  • Los procesos hijo son interrumpibles / killables.
  • El módulo de multiprocessing Python incluye abstracciones útiles con una interfaz muy parecida a la de threading.Thread
  • Una necesidad con cPython para el procesamiento enlazado a la CPU

Contras

  • IPC un poco más complicado con más sobrecarga (modelo de comunicación vs. memoria / objetos compartidos)
  • Huella de memoria más grande

Enhebrado

Pros

  • Ligero – espacio de memoria bajo
  • Memoria compartida: facilita el acceso al estado desde otro contexto
  • Le permite crear fácilmente interfaces de usuario sensibles
  • Los módulos de extensión cPython C que lanzan correctamente GIL se ejecutarán en paralelo
  • Gran opción para aplicaciones enlazadas de E / S

Contras

  • cPython – sujeto a la GIL
  • No interrumpible / killable
  • Si no sigue una cola de comandos / modelo de bomba de mensaje (utilizando el módulo de Queue ), el uso manual de las primitivas de sincronización se convierte en una necesidad (las decisiones son necesarias para la granularidad del locking)
  • El código suele ser más difícil de entender y de hacer bien: el potencial de las condiciones de la carrera aumenta dramáticamente

El trabajo de Threading es permitir que las aplicaciones respondan. Supongamos que tiene una conexión de base de datos y necesita responder a las entradas del usuario. Sin subprocesos, si la conexión de la base de datos está ocupada, la aplicación no podrá responder al usuario. Al dividir la conexión de la base de datos en un hilo separado, puede hacer que la aplicación sea más receptiva. Además, como ambos subprocesos están en el mismo proceso, pueden acceder a las mismas estructuras de datos: buen rendimiento y un diseño de software flexible.

Tenga en cuenta que, debido a la GIL, la aplicación no está haciendo dos cosas a la vez, pero lo que hemos hecho es poner el locking de recursos en la base de datos en un subproceso separado para que el tiempo de CPU se pueda cambiar entre esta y la interacción del usuario. El tiempo de CPU se raciona entre los hilos.

El multiprocesamiento es para los momentos en que realmente desea que se haga más de una cosa en un momento dado. Supongamos que su aplicación necesita conectarse a 6 bases de datos y realizar una transformación de matriz compleja en cada conjunto de datos. Poner cada trabajo en un subproceso separado puede ayudar un poco porque cuando una conexión está inactiva, otra puede obtener algo de tiempo de CPU, pero el procesamiento no se realizará en paralelo porque GIL significa que solo está usando los recursos de una CPU . Al poner cada trabajo en un proceso de multiprocesamiento, cada uno puede ejecutarse en su propia CPU y ejecutarse con la máxima eficiencia.

La ventaja clave es el aislamiento. Un proceso que falla no derribará otros procesos, mientras que un subproceso que causa un estruendo probablemente causará esgulps en otros subprocesos.

Otra cosa que no se menciona es que depende de qué sistema operativo esté utilizando en lo que respecta a la velocidad. En Windows, los procesos son costosos, por lo que los subprocesos serían mejores en Windows, pero en Unix los procesos son más rápidos que sus variantes de Windows, por lo que usar procesos en Unix es mucho más seguro y rápido de generar.

Otras respuestas se han centrado más en el aspecto multiproceso frente al multiprocesamiento, pero en Python Global Interpreter Lock ( GIL ) debe tenerse en cuenta. Cuando se crea más cantidad (por ejemplo, k ) de subprocesos, generalmente no boostán el rendimiento k veces, ya que todavía se ejecutará como una aplicación de un solo subproceso. GIL es un locking global que bloquea todo y permite solo la ejecución de un solo hilo utilizando un solo núcleo. El rendimiento aumenta en los lugares donde se utilizan las extensiones C, como el número, la red, la E / S, donde se realiza una gran cantidad de trabajo en segundo plano y se libera GIL.
Por lo tanto, cuando se utiliza el subprocesamiento , solo hay un único subproceso a nivel del sistema operativo, mientras que Python crea pseudohilos que se administran completamente mediante el subproceso en sí, pero que se ejecutan esencialmente como un solo proceso. La preferencia tiene lugar entre estos pseudo hilos. Si la CPU se ejecuta a su capacidad máxima, es posible que desee cambiar al multiprocesamiento.
Ahora, en el caso de instancias de ejecución autocontenidas, puede optar por un pool. Pero en el caso de datos superpuestos, donde es posible que desee que los procesos se comuniquen, debe utilizar el proceso de multiprocessing.Process .

Como se mencionó en la pregunta, el multiprocesamiento en Python es la única forma real de lograr un verdadero paralelismo. El subprocesamiento múltiple no puede lograr esto porque GIL evita que los subprocesos se ejecuten en paralelo.

Como consecuencia, los subprocesos pueden no ser siempre útiles en Python y, de hecho, incluso pueden dar como resultado un peor rendimiento en función de lo que se está tratando de lograr. Por ejemplo, si está realizando una tarea vinculada a la CPU , como descomprimir archivos gzip o renderizado en 3D (cualquier cosa que requiera mucho uso de la CPU), los subprocesos pueden dificultar su rendimiento en lugar de ayudar. En tal caso, desearía utilizar el Multiprocesamiento ya que solo este método se ejecuta en paralelo y ayudará a distribuir el peso de la tarea en cuestión. Podría haber algo de sobrecarga en esto, ya que el Multiprocesamiento implica copiar la memoria de un script en cada subproceso, lo que puede causar problemas en aplicaciones de mayor tamaño.

Sin embargo, Multithreading se vuelve útil cuando su tarea está vinculada a IO . Por ejemplo, si la mayor parte de su tarea implica esperar en llamadas a la API , usaría Multithreading porque, por qué no iniciar otra solicitud en otro subproceso mientras espera, en lugar de hacer que la CPU permanezca inactiva.

TL; DR

  • El subprocesamiento múltiple es concurrente y se utiliza para tareas vinculadas a IO
  • El multiprocesamiento logra un verdadero paralelismo y se utiliza para tareas relacionadas con la CPU

El proceso puede tener múltiples hilos. Estos hilos pueden compartir memoria y son las unidades de ejecución dentro de un proceso.

Los procesos se ejecutan en la CPU, por lo que los subprocesos residen en cada proceso. Los procesos son entidades individuales que se ejecutan de forma independiente. Si desea compartir datos o estados entre cada proceso, puede usar una herramienta de almacenamiento de memoria como Cache(redis, memcache) , Files o una Database .

Cotizaciones de documentación de Python

He resaltado las citas clave de la documentación de Python sobre Process vs Threads y GIL en: ¿Qué es el locking de intérprete global (GIL) en CPython?

Proceso vs experimentos de hilo

Hice un poco de benchmarking para mostrar la diferencia más concretamente.

En el punto de referencia, cronometré el trabajo encuadernado de CPU y de E / S para varios números de subprocesos en una CPU de 8 hilos. El trabajo suministrado es el mismo para cada número de hilos (por lo tanto, más hilos significa más trabajo total suministrado).

Los resultados fueron:

introduzca la descripción de la imagen aquí

Conclusiones:

  • para el trabajo con CPU, el multiprocesamiento siempre es más rápido, probablemente debido a la GIL

  • para trabajo encuadernado IO. ambos son exactamente la misma velocidad

  • los subprocesos solo se pueden ampliar hasta aproximadamente 4x en lugar de los 8x esperados ya que estoy en una máquina con 8 hilos.

    Contrasta esto con un trabajo vinculado a la CPU de C POSIX que alcanza la aceleración esperada de 8x: ¿Qué significan ‘real’, ‘usuario’ y ‘sys’ en la salida del tiempo (1)?

    TODO: No sé la razón de esto, debe haber otras ineficiencias de Python entrando en juego.

Código de prueba:

 #!/usr/bin/env python3 import multiprocessing import threading import time import sys def cpu_func(result, niters): ''' A useless CPU bound function. ''' for i in range(niters): result = (result * result * i + 2 * result * i * i + 3) % 10000000 return result class CpuThread(threading.Thread): def __init__(self, niters): super().__init__() self.niters = niters self.result = 1 def run(self): self.result = cpu_func(self.result, self.niters) class CpuProcess(multiprocessing.Process): def __init__(self, niters): super().__init__() self.niters = niters self.result = 1 def run(self): self.result = cpu_func(self.result, self.niters) class IoThread(threading.Thread): def __init__(self, sleep): super().__init__() self.sleep = sleep self.result = self.sleep def run(self): time.sleep(self.sleep) class IoProcess(multiprocessing.Process): def __init__(self, sleep): super().__init__() self.sleep = sleep self.result = self.sleep def run(self): time.sleep(self.sleep) if __name__ == '__main__': cpu_n_iters = int(sys.argv[1]) sleep = 1 cpu_count = multiprocessing.cpu_count() input_params = [ (CpuThread, cpu_n_iters), (CpuProcess, cpu_n_iters), (IoThread, sleep), (IoProcess, sleep), ] header = ['nthreads'] for thread_class, _ in input_params: header.append(thread_class.__name__) print(' '.join(header)) for nthreads in range(1, 2 * cpu_count): results = [nthreads] for thread_class, work_size in input_params: start_time = time.time() threads = [] for i in range(nthreads): thread = thread_class(work_size) threads.append(thread) thread.start() for i, thread in enumerate(threads): thread.join() results.append(time.time() - start_time) print(' '.join('{:.6e}'.format(result) for result in results)) 

GitHub upstream + plotting code en el mismo directorio .

Python 3.6.7, probado en Ubuntu 18.10, en una laptop 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).

MULTIPROCESAMIENTO

  • El multiprocesamiento agrega CPU para boost la potencia de cálculo.
  • Múltiples procesos se ejecutan al mismo tiempo.
  • La creación de un proceso requiere mucho tiempo y recursos.
  • El multiprocesamiento puede ser simétrico o asimétrico.
  • La biblioteca de multiprocesamiento en Python utiliza espacio de memoria separado, múltiples núcleos de CPU, evita las limitaciones de GIL en CPython, los procesos secundarios son mulables (por ejemplo, llamadas a funciones en el progtwig) y es mucho más fácil de usar.
  • Algunas advertencias del módulo son una mayor huella de memoria e IPC es un poco más complicado con más sobrecarga.

MULTITHREADING

  • El multihilo crea múltiples hilos de un solo proceso para boost la potencia de cálculo.
  • Múltiples hilos de un solo proceso se ejecutan al mismo tiempo.
  • La creación de un hilo es económica tanto en tiempo como en recursos.
  • La biblioteca de subprocesos múltiples es liviana, comparte memoria, es responsable de la interfaz de usuario receptiva y se usa bien para aplicaciones enlazadas de E / S.
  • El módulo no es killable y está sujeto a la GIL.
  • Varios subprocesos viven en el mismo proceso en el mismo espacio, cada subproceso realizará una tarea específica, tendrá su propio código, su propia memoria de stack, el puntero de instrucción y compartirá la memoria del montón.
  • Si un hilo tiene una pérdida de memoria, puede dañar los otros hilos y el proceso principal.

Ejemplo de multihilo y multiprocesamiento utilizando Python

Python 3 tiene la facilidad de lanzar tareas paralelas . Esto facilita nuestro trabajo.

Tiene para agrupación de hilos y agrupación de procesos .

Lo siguiente da una idea:

Ejemplo de ThreadPoolExecutor

 import concurrent.futures import urllib.request URLS = ['http://www.foxnews.com/', 'http://www.cnn.com/', 'http://europe.wsj.com/', 'http://www.bbc.co.uk/', 'http://some-made-up-domain.com/'] # Retrieve a single page and report the URL and contents def load_url(url, timeout): with urllib.request.urlopen(url, timeout=timeout) as conn: return conn.read() # We can use a with statement to ensure threads are cleaned up promptly with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: # Start the load operations and mark each future with its URL future_to_url = {executor.submit(load_url, url, 60): url for url in URLS} for future in concurrent.futures.as_completed(future_to_url): url = future_to_url[future] try: data = future.result() except Exception as exc: print('%r generated an exception: %s' % (url, exc)) else: print('%r page is %d bytes' % (url, len(data))) 

ProcessPoolExecutor

 import concurrent.futures import math PRIMES = [ 112272535095293, 112582705942171, 112272535095293, 115280095190773, 115797848077099, 1099726899285419] def is_prime(n): if n % 2 == 0: return False sqrt_n = int(math.floor(math.sqrt(n))) for i in range(3, sqrt_n + 1, 2): if n % i == 0: return False return True def main(): with concurrent.futures.ProcessPoolExecutor() as executor: for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)): print('%d is prime: %s' % (number, prime)) if __name__ == '__main__': main()