Concurrencia en Python: multiprocesamiento

En este capítulo, nos centraremos más en la comparación entre multiprocesamiento y multiproceso.

Multiprocesamiento

Es el uso de dos o más CPU dentro de un solo sistema informático. Es el mejor enfoque para aprovechar todo el potencial de nuestro hardware utilizando el número total de núcleos de CPU disponibles en nuestro sistema informático.

Multihilo

Es la capacidad de una CPU para administrar el uso del sistema operativo ejecutando varios subprocesos al mismo tiempo. La idea principal del subproceso múltiple es lograr el paralelismo dividiendo un proceso en varios subprocesos.

La siguiente tabla muestra algunas de las diferencias importantes entre ellos:

Multiprocesamiento Multiprogramación
El multiprocesamiento se refiere al procesamiento de múltiples procesos al mismo tiempo por múltiples CPU. La multiprogramación mantiene varios programas en la memoria principal al mismo tiempo y los ejecuta simultáneamente utilizando una sola CPU.
Utiliza varias CPU. Utiliza una sola CPU.
Permite el procesamiento paralelo. Se produce un cambio de contexto.
Menos tiempo para procesar los trabajos. Más tiempo necesario para procesar los trabajos.
Facilita una utilización mucho más eficiente de los dispositivos del sistema informático. Menos eficiente que el multiprocesamiento.
Suele ser más caro. Estos sistemas son menos costosos.

Eliminando el impacto del bloqueo de intérprete global (GIL)

Al trabajar con aplicaciones concurrentes, existe una limitación presente en Python llamada GIL (Global Interpreter Lock). GIL nunca nos permite utilizar múltiples núcleos de CPU y, por lo tanto, podemos decir que no hay verdaderos hilos en Python. GIL es el mutex - bloqueo de exclusión mutua, que hace que las cosas sean seguras para subprocesos. En otras palabras, podemos decir que GIL evita que varios subprocesos ejecuten código Python en paralelo. El bloqueo puede ser retenido por un solo hilo a la vez y si queremos ejecutar un hilo, primero debe adquirir el bloqueo.

Con el uso del multiprocesamiento, podemos evitar de manera efectiva la limitación causada por GIL:

  • Al usar multiprocesamiento, estamos utilizando la capacidad de múltiples procesos y, por lo tanto, estamos utilizando múltiples instancias de GIL.

  • Debido a esto, no existe ninguna restricción para ejecutar el código de bytes de un hilo dentro de nuestros programas en cualquier momento.

Iniciar procesos en Python

Los siguientes tres métodos se pueden utilizar para iniciar un proceso en Python dentro del módulo de multiprocesamiento:

  • Fork
  • Spawn
  • Forkserver

Creando un proceso con Fork

El comando Fork es un comando estándar que se encuentra en UNIX. Se utiliza para crear nuevos procesos denominados procesos secundarios. Este proceso hijo se ejecuta al mismo tiempo que el proceso denominado proceso padre. Estos procesos secundarios también son idénticos a sus procesos principales y heredan todos los recursos disponibles para el principal. Las siguientes llamadas al sistema se utilizan al crear un proceso con Fork:

  • fork()- Es una llamada al sistema generalmente implementada en el kernel. Se utiliza para crear una copia del proceso. P>

  • getpid() - Esta llamada al sistema devuelve el ID de proceso (PID) del proceso de llamada.

Ejemplo

El siguiente ejemplo de secuencia de comandos de Python le ayudará a entender cómo crear un nuevo proceso hijo y obtener los PID de los procesos hijo y padre:

import os

def child():
   n = os.fork()
   
   if n > 0:
      print("PID of Parent process is : ", os.getpid())

   else:
      print("PID of Child process is : ", os.getpid())
child()

Salida

PID of Parent process is : 25989
PID of Child process is : 25990

Creando un proceso con Spawn

Spawn significa comenzar algo nuevo. Por lo tanto, generar un proceso significa la creación de un nuevo proceso por un proceso padre. El proceso padre continúa su ejecución de forma asincrónica o espera hasta que el proceso hijo finaliza su ejecución. Siga estos pasos para generar un proceso:

  • Importando módulo de multiprocesamiento.

  • Creación del proceso del objeto.

  • Iniciar la actividad del proceso llamando start() método.

  • Esperar hasta que el proceso haya terminado su trabajo y salir llamando join() método.

Ejemplo

El siguiente ejemplo de secuencia de comandos de Python ayuda a generar tres procesos

import multiprocessing

def spawn_process(i):
   print ('This is process: %s' %i)
   return

if __name__ == '__main__':
   Process_jobs = []
   for i in range(3):
   p = multiprocessing.Process(target = spawn_process, args = (i,))
      Process_jobs.append(p)
   p.start()
   p.join()

Salida

This is process: 0
This is process: 1
This is process: 2

Creando un proceso con Forkserver

El mecanismo de bifurcación solo está disponible en aquellas plataformas UNIX seleccionadas que admiten el paso de descriptores de archivo a través de Unix Pipes. Considere los siguientes puntos para comprender el funcionamiento del mecanismo Forkserver:

  • Se crea una instancia de un servidor al utilizar el mecanismo Forkserver para iniciar un nuevo proceso.

  • Luego, el servidor recibe el comando y maneja todas las solicitudes para crear nuevos procesos.

  • Para crear un nuevo proceso, nuestro programa Python enviará una solicitud a Forkserver y creará un proceso para nosotros.

  • Por fin, podemos utilizar este nuevo proceso creado en nuestros programas.

Procesos daemon en Python

Pitón multiprocessingEl módulo nos permite tener procesos daemon a través de su opción daemonic. Los procesos daemon o los procesos que se ejecutan en segundo plano siguen un concepto similar al de los subprocesos daemon. Para ejecutar el proceso en segundo plano, necesitamos establecer el indicador daemonic en verdadero. El proceso del demonio continuará ejecutándose mientras se esté ejecutando el proceso principal y terminará después de finalizar su ejecución o cuando el programa principal sea eliminado.

Ejemplo

Aquí, estamos usando el mismo ejemplo que se usó en los hilos del demonio. La única diferencia es el cambio de módulo demultithreading a multiprocessingy estableciendo el indicador daemonic en verdadero. Sin embargo, habría un cambio en la salida como se muestra a continuación:

import multiprocessing
import time

def nondaemonProcess():
   print("starting my Process")
   time.sleep(8)
   print("ending my Process")
def daemonProcess():
   while True:
   print("Hello")
   time.sleep(2)
if __name__ == '__main__':
   nondaemonProcess = multiprocessing.Process(target = nondaemonProcess)
   daemonProcess = multiprocessing.Process(target = daemonProcess)
   daemonProcess.daemon = True
   nondaemonProcess.daemon = False
   daemonProcess.start()
   nondaemonProcess.start()

Salida

starting my Process
ending my Process

La salida es diferente en comparación con la generada por los subprocesos del demonio, porque el proceso en modo sin demonio tiene una salida. Por tanto, el proceso demoníaco finaliza automáticamente después de que finalizan los programas principales para evitar la persistencia de los procesos en ejecución.

Terminar procesos en Python

Podemos matar o terminar un proceso inmediatamente usando el terminate()método. Usaremos este método para terminar el proceso hijo, que ha sido creado con la ayuda de function, inmediatamente antes de completar su ejecución.

Ejemplo

import multiprocessing
import time
def Child_process():
   print ('Starting function')
   time.sleep(5)
   print ('Finished function')
P = multiprocessing.Process(target = Child_process)
P.start()
print("My Process has terminated, terminating main thread")
print("Terminating Child Process")
P.terminate()
print("Child Process successfully terminated")

Salida

My Process has terminated, terminating main thread
Terminating Child Process
Child Process successfully terminated

La salida muestra que el programa termina antes de la ejecución del proceso hijo que se ha creado con la ayuda de la función Child_process (). Esto implica que el proceso hijo se ha terminado correctamente.

Identificar el proceso actual en Python

Todos los procesos del sistema operativo tienen una identidad de proceso conocida como PID. En Python, podemos averiguar el PID del proceso actual con la ayuda del siguiente comando:

import multiprocessing
print(multiprocessing.current_process().pid)

Ejemplo

El siguiente ejemplo de secuencia de comandos de Python ayuda a averiguar el PID del proceso principal, así como el PID del proceso hijo:

import multiprocessing
import time
def Child_process():
   print("PID of Child Process is: {}".format(multiprocessing.current_process().pid))
print("PID of Main process is: {}".format(multiprocessing.current_process().pid))
P = multiprocessing.Process(target=Child_process)
P.start()
P.join()

Salida

PID of Main process is: 9401
PID of Child Process is: 9402

Usando un proceso en subclase

Podemos crear hilos subclasificando el threading.Threadclase. Además, también podemos crear procesos subclasificando elmultiprocessing.Processclase. Para usar un proceso en una subclase, debemos considerar los siguientes puntos:

  • Necesitamos definir una nueva subclase del Process clase.

  • Necesitamos anular el _init_(self [,args] ) clase.

  • Necesitamos anular el de la run(self [,args] ) método para implementar lo que Process

  • Necesitamos comenzar el proceso invocando elstart() método.

Ejemplo

import multiprocessing
class MyProcess(multiprocessing.Process):
   def run(self):
   print ('called run method in process: %s' %self.name)
   return
if __name__ == '__main__':
   jobs = []
   for i in range(5):
   P = MyProcess()
   jobs.append(P)
   P.start()
   P.join()

Salida

called run method in process: MyProcess-1
called run method in process: MyProcess-2
called run method in process: MyProcess-3
called run method in process: MyProcess-4
called run method in process: MyProcess-5

Módulo de multiprocesamiento de Python - Clase de grupo

Si hablamos de paralelo simple processingtareas en nuestras aplicaciones Python, luego el módulo de multiprocesamiento nos proporciona la clase Pool. Los siguientes métodos dePool La clase se puede usar para aumentar el número de procesos secundarios dentro de nuestro programa principal.

aplicar () método

Este método es similar al.submit()método de .ThreadPoolExecutor.Bloquea hasta que el resultado está listo.

método apply_async ()

Cuando necesitamos la ejecución paralela de nuestras tareas, necesitamos usar elapply_async()método para enviar tareas al grupo. Es una operación asincrónica que no bloqueará el hilo principal hasta que se ejecuten todos los procesos secundarios.

método map ()

Como el apply()método, también bloquea hasta que el resultado está listo. Es equivalente al incorporadomap() función que divide los datos iterables en varios fragmentos y los envía al grupo de procesos como tareas independientes.

método map_async ()

Es una variante del map() método como apply_async() es para el apply()método. Devuelve un objeto de resultado. Cuando el resultado está listo, se le aplica un invocable. El invocable debe completarse inmediatamente; de lo contrario, el hilo que maneja los resultados se bloqueará.

Ejemplo

El siguiente ejemplo le ayudará a implementar un grupo de procesos para realizar una ejecución en paralelo. Se ha realizado un cálculo simple del cuadrado del número aplicando elsquare() función a través del multiprocessing.Poolmétodo. Luegopool.map() se ha utilizado para enviar el 5, porque la entrada es una lista de números enteros del 0 al 4. El resultado se almacenaría en p_outputs y está impreso.

def square(n):
   result = n*n
   return result
if __name__ == '__main__':
   inputs = list(range(5))
   p = multiprocessing.Pool(processes = 4)
   p_outputs = pool.map(function_square, inputs)
   p.close()
   p.join()
   print ('Pool :', p_outputs)

Salida

Pool : [0, 1, 4, 9, 16]