Course
Tutorial de multiprocesamiento en Python
La biblioteca estándar de Python viene equipada con varios paquetes incorporados para que los desarrolladores empiecen a aprovechar las ventajas del lenguaje al instante. Uno de estos paquetes es el módulo de multiprocesamiento, que permite a los sistemas ejecutar varios procesos simultáneamente. En otras palabras, los desarrolladores pueden dividir las aplicaciones en subprocesos más pequeños que pueden ejecutarse independientemente de su código Python. A continuación, el sistema operativo asigna estos hilos o procesos al procesador, lo que les permite ejecutarse en paralelo y mejorar el rendimiento y la eficacia de tus programas Python.
Si palabras como hilos, procesos, procesadores, etc. no le resultan familiares, no se preocupe. En este artículo, vamos a cubrir la definición de un proceso, en qué se diferencian de los hilos, y cómo utilizar el módulo de multiprocesamiento.
Echa un vistazo a este libro de trabajo de DataLab para seguir el código de este tutorial.
¿Qué son los procesos en Python?
Entender el concepto de procesos e hilos es extremadamente útil para comprender mejor cómo un sistema operativo gestiona los programas a través de las distintas etapas de ejecución. Un proceso no es más que una referencia a un programa informático. Cada programa tiene asociado un proceso.
Mientras lees este artículo, es muy probable que tu ordenador tenga muchos procesos en ejecución en un momento dado, aunque sólo tengas unos pocos programas abiertos; esto se debe a que la mayoría de los sistemas operativos tienen varias tareas ejecutándose en segundo plano. Por ejemplo, puede que sólo tenga tres programas en ejecución en este momento, pero su ordenador puede tener más de 30 procesos activos ejecutándose simultáneamente.
La forma de comprobar los procesos activos que se están ejecutando en su ordenador depende de su sistema operativo:
- En Windows: Ctrl+Shift+Esc iniciará el administrador de tareas
- En Mac: abre Spotlight Search en Mac y escribe "Monitor de actividad", luego pulsa Retorno
- En Linux: haga clic en Menú de aplicaciones y busque Monitor del sistema.
Python proporciona acceso a procesos reales a nivel de sistema. Instanciar una instancia de la clase Process desde el módulo de multiprocesamiento permite a los desarrolladores hacer referencia al proceso nativo subyacente utilizando Python. Se crea un nuevo proceso nativo entre bastidores cuando se inicia un proceso. El ciclo de vida de un proceso Python consta de tres etapas: La iniciación de un nuevo proceso, el proceso en ejecución, y el proceso terminado - cubriremos cada etapa en Los fundamentos del módulo de multiprocesamiento de Python.
Todos los procesos están formados por uno o más hilos. ¿Recuerda que mencionamos que "un proceso no es más que una referencia a un programa informático"? Pues bien, cada programa Python es un proceso que consta de un hilo por defecto llamado hilo principal. El hilo principal es el responsable de ejecutar las instrucciones dentro de tus programas Python. Sin embargo, es importante tener en cuenta que los procesos y los hilos son diferentes.
Multiprocesamiento frente a Enhebrado
Para ser más concretos, una instancia del intérprete de Python -la herramienta que convierte el código escrito en Python al lenguaje que puede entender un ordenador- equivale a un proceso. Un proceso constará de al menos un hilo, llamado "hilo principal" en Python, aunque pueden crearse otros hilos dentro del mismo proceso - todos los demás hilos creados dentro de un proceso pertenecerán a ese proceso.
El hilo sirve como representación de cómo se ejecutará tu programa Python, y una vez que todos los hilos que no están en segundo plano hayan terminado, el proceso Python terminará.
- Proceso: Un proceso es una instancia del intérprete de Python que consiste en al menos un hilo llamado hilo principal.
- Hilo: Una representación de cómo se ejecuta un programa Python dentro de un proceso Python.
Python tiene dos clases extremadamente similares que nos otorgan más control sobre los procesos y los hilos: multiprocessing.Process y threading.Thread.
Repasemos algunas de sus similitudes y diferencias.
Similitudes
#1 Concurrencia
La concurrencia es un concepto en el que diferentes partes del programa pueden ejecutarse fuera de orden o en orden parcial sin que el resultado final se vea afectado. Ambas clases estaban pensadas inicialmente para la concurrencia.
#2 Compatibilidad con primitivas de concurrencia
Las clases multiprocessing.Process y threading.Thread soportan las mismas primitivas de concurrencia - una herramienta que permite la sincronización y coordinación de hilos y procesos.
#3 API uniforme
Una vez que hayas comprendido la API multiprocessing.Process, podrás transferir esos conocimientos a la API threading.Thread, y viceversa. Se diseñaron así intencionadamente.
Diferencias
#1 Funcionalidad
A pesar de que sus API son las mismas, los procesos y los hilos son diferentes. Un proceso es un nivel de abstracción superior a un hilo: un proceso es una referencia a un programa informático, y un hilo pertenece a un proceso. Esta diferencia es inherente a las clases. Así, las clases representan dos funciones nativas diferentes gestionadas por un sistema operativo subyacente.
#2 Acceso al estado compartido
Las dos clases acceden al estado compartido de forma diferente. Dado que los hilos pertenecen a un proceso, pueden compartir memoria dentro de un proceso. Así, una función ejecutada en un nuevo hilo sigue teniendo acceso a los mismos datos y estado dentro de un proceso. La forma en que los hilos comparten estados entre sí se conoce como "memoria compartida" y es bastante sencilla. Por el contrario, compartir estados entre procesos es mucho más complicado: el estado debe serializarse y transmitirse entre procesos. En otras palabras, los procesos no utilizan memoria compartida para compartir estados porque tienen memoria separada. En su lugar, los procesos se comparten utilizando una técnica llamada "comunicación entre procesos", y para realizarla en Python se requieren otras herramientas explícitas como multiprocessing.Pipe o multiprocessing.Queue.
#3 GIL
El Bloqueo Global del Intérprete de Python (GIL) es un bloqueo que permite que sólo un hilo mantenga el control sobre el intérprete de Python. Los hilos múltiples están sujetos a la GIL, lo que a menudo hace que usar Python para realizar multihilos sea una mala idea: la verdadera ejecución multinúcleo a través de multihilos no está soportada por Python en el intérprete CPython. Sin embargo, los procesos no están sujetos al GIL porque el GIL se utiliza dentro de cada proceso Python pero no entre procesos.
En términos de casos de uso, el multiprocesamiento suele eclipsar al threading en escenarios en los que el programa hace un uso intensivo de la CPU y no es necesario que realice ninguna E/S o interacción con el usuario. Los subprocesos son la mejor solución para los programas que están ligados a la E/S o a la red y en escenarios en los que el objetivo es hacer que la aplicación responda mejor.
Ventajas del multiprocesamiento en Python
Piense en un procesador como en un empresario. A medida que el negocio del empresario crece, hay más tareas que deben gestionarse para mantener el ritmo de crecimiento de la empresa. Si la empresaria decide asumir sola todas estas tareas (contabilidad, ventas, marketing, innovación, etc.), corre el riesgo de obstaculizar la eficacia y el rendimiento generales de la empresa, ya que una sola persona sólo puede hacer una cantidad limitada de cosas a la vez. Por ejemplo, antes de pasar a las tareas de innovación, debe detener las tareas de ventas, lo que se conoce como ejecutar las tareas "secuencialmente".
La mayoría de los empresarios entienden que intentar hacerlo todo solos es una mala idea. En consecuencia, suelen compensar el creciente número de tareas contratando empleados para gestionar varios departamentos. De este modo, las tareas pueden realizarse en paralelo, es decir, no es necesario detener una tarea para que se ejecute otra. Contratar a más empleados para realizar tareas específicas es como utilizar varios procesadores para llevar a cabo las operaciones. Por ejemplo, los proyectos de visión por ordenador son bastante exigentes, ya que normalmente hay que procesar muchos datos de imágenes, lo que lleva mucho tiempo: para acelerar este procedimiento, se podrían procesar varias imágenes en paralelo.
Así, podemos decir que el multiprocesamiento sirve para hacer más eficientes los programas dividiendo y asignando tareas a distintos procesadores. El módulo de multiprocesamiento de Python simplifica aún más esta tarea al servir como una herramienta de alto nivel para aumentar la eficiencia de sus programas mediante la asignación de tareas a diferentes procesos.
Conceptos básicos del módulo de multiprocesamiento de Python
En la sección "¿Qué son los procesos en Python?", mencionamos que el ciclo de vida de un proceso Python consta de tres etapas: el proceso nuevo, el proceso en ejecución y el proceso finalizado. Esta sección profundizará en cada fase del ciclo de vida y proporcionará ejemplos codificados.
El nuevo proceso
Un nuevo proceso puede definirse como el proceso que se ha creado instanciando una instancia de la clase Proceso. Se genera un proceso hijo cuando asignamos el objeto Proceso a una variable.
from multiprocessing import Process
# Create a new process
process = Process()
Ahora mismo, nuestra instancia de proceso no está haciendo nada porque hemos inicializado un objeto Proceso vacío. Podríamos alterar las configuraciones de nuestro objeto Process para ejecutar una función específica pasando una función que queremos ejecutar en un proceso diferente al parámetro target de la clase.
# Create a new process with a specified function to execute.
def example_function():
pass
new_process = Process(target=example_function)
Si nuestra función de destino también tuviera parámetros, simplemente los pasaríamos al parámetro args del objeto Process como una tupla.
Consejo: Aprenda a escribir funciones en Python con el curso interactivo Escribir funciones en Python.
# Create a new process with specific function to execute with args.
def example(args):
pass
process = Process(target=example, args=("Hi",))
Tenga en cuenta que sólo hemos creado un nuevo proceso, pero aún no se está ejecutando.
Veamos cómo podemos ejecutar un nuevo proceso.
El proceso en marcha
Ejecutar un nuevo proceso es bastante sencillo: basta con llamar al método start() de la instancia del proceso.
# Run the new process
process.start()
Esta acción inicia la actividad del proceso llamando al método run() de la instancia Process bajo el capó. El método run() también es responsable de llamar a la función personalizada especificada en el parámetro target de la instancia Process (si se ha especificado).
Recuerde que anteriormente en el tutorial, dijimos que cada proceso tiene al menos un hilo llamado hilo principal - este es el predeterminado. Así, cuando se inicia un proceso hijo, se crea el hilo principal para ese proceso hijo y se inicia. El hilo principal es responsable de ejecutar todo nuestro código en el proceso hijo.
Podemos comprobar que nuestra instancia de proceso está viva desde que el método start() retorna hasta que el proceso hijo termina usando el método is_alive() en nuestra instancia de Proceso.
Nota: Si estás probando esto en un cuaderno, la llamada a is_alive() debe estar en la misma celda que la llamada al método start() para que atrape el proceso en ejecución.
# Run the new process
process.start()
# Check process is running
process.is_alive()
"""
True
"""
Si el proceso no se estuviera ejecutando, la llamada al método is_alive() devolvería False.
El proceso finalizado
Cuando la función run() retorna o sale, el proceso se termina: no necesariamente tenemos que hacer algo explícitamente. En otras palabras, puede esperar que un proceso termine después de que se completen las instrucciones establecidas como función de destino. Por lo tanto, el método is_alive() devolvería false.
# Check process is terminated - should return false.
process.is_alive()
"""
False
"""
Sin embargo, un proceso también puede terminar si encuentra una excepción no controlada o se produce un error. Por ejemplo, si se produce un error en la función establecida como objetivo, el proceso finalizará.
# Create a new process with a specific function that has an error
# to execute with args.
def example(args):
split_args = list(args.split())
# "name" variable is not in the function namespace - should raise error
return name
# New process
process = Process(target=example, args=("Hi",))
# Running the new process
process.start()
# Check process is running
process.is_alive()
"""
True
Process Process-15:
Traceback (most recent call last):
File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
self.run()
"""
El objeto Process también tiene los métodos terminate() y kill() que permiten a los usuarios terminar un proceso a la fuerza.
# Create a new process with a specific function to execute with args.
def example(args):
split_args = list(args.split())
# "name" variable is not in the function namespace - should raise error
return split_args
# New process
process = Process(target=example, args=("Hi",))
# Running the new process
process.start()
if process.is_alive():
process.terminate() # You can also use process.kill()
print("Process terminated forcefully")
"""
Process terminated forcefully
"""
Es importante tener en cuenta que las terminaciones forzadas no son la forma recomendada de terminar un proceso: pueden no cerrar todos los recursos abiertos de forma segura o almacenar el estado requerido del programa. Una solución mejor es utilizar un apagado controlado con un indicador booleano de proceso seguro o una herramienta similar.
Tutorial de multiprocesamiento en Python
Ahora que entiendes los fundamentos del multiprocesamiento, vamos a trabajar en un ejemplo para demostrar cómo hacer programación concurrente en Python.
La función que creamos simplemente imprimirá una declaración, dormirá durante 1 segundo, y luego imprimirá otra dormida - aprende más sobre funciones en este tutorial de funciones de Python.
import time
def do_something():
print("I'm going to sleep")
time.sleep(1)
print("I'm awake")
El primer paso es crear un nuevo proceso: vamos a crear dos.
# Create new child process
process_1 = Process(target=do_something)
process_2 = Process(target=do_something)
Así es como se verá un programa concurrente en un entorno de cuaderno:
%%time
# Starts both processes
process_1.start()
process_2.start()
"""
I'm going to sleep
CPU times: user 810 µs, sys: 7.34 ms, total: 8.15 ms
Wall time: 6.04 ms
I'm going to sleep
"""
Dada la salida del programa, es bastante evidente que hay un problema en alguna parte de nuestro código. El temporizador se imprime a mitad de nuestro primer proceso, y la segunda sentencia print no se imprime.
Esto ocurre porque hay tres procesos en ejecución: proceso principal, proceso_1 y proceso_2. El proceso que hace el seguimiento del tiempo y lo imprime es el proceso principal. Para que nuestro proceso principal espere antes de imprimir la hora, debemos llamar al método join() en nuestros dos procesos después de ejecutarlos.
Nota: Si quieres saber más, consulta este debate en Stackoverflow.
Veamos nuestro nuevo fragmento de código:
%%time
# Create new child process (Cannot run a process more than once)
new_process_1 = Process(target=do_something)
new_process_2 = Process(target=do_something)
# Starts both processes
new_process_1.start()
new_process_2.start()
new_process_1.join()
new_process_2.join()
"""
I'm going to sleep
I'm going to sleep
I'm awake
I'm awake
CPU times: user 0 ns, sys: 14 ms, total: 14 ms
Wall time: 1.01 s
"""
Problema resuelto.
El tiempo de pared de este recorrido fue ligeramente superior al del primer recorrido. Sin embargo, esta ejecución completó las llamadas a nuestras dos funciones objetivo antes de devolver la información horaria. También podemos aplicar este mismo razonamiento para hacer que más procesos se ejecuten simultáneamente.
Envolver
En este tutorial, has aprendido cómo hacer que los programas Python sean más eficientes ejecutándolos de forma concurrente. Específicamente, aprendiste:
- Qué son los procesos y cómo puedes verlos en tu ordenador.
- Similitudes y diferencias entre los módulos de multiprocesamiento e hilos de Python.
- Los fundamentos del módulo de multiprocesamiento y cómo ejecutar un programa Python de forma concurrente utilizando el multiprocesamiento.
¿Quiere saber más sobre la programación en Python? Echa un vistazo al Curso de Programador Python de Datacamp. No se requiere experiencia previa en programación y, al final del curso, habrá adquirido los conocimientos necesarios para desarrollar software, manejar datos y realizar análisis avanzados en Python.
Cursos para Python
Course
Introduction to Data Science in Python
Course
Visualizing Time Series Data in Python
tutorial
Una Introducción al Subproceso Python: Conceptos básicos y ejemplos
tutorial
Tutorial sobre cómo trabajar con módulos en Python
Nishant Kumar
8 min
tutorial
Tutorial de funciones de Python
tutorial
Tutorial sobre el uso de XGBoost en Python
Bekhruz Tuychiev
16 min
tutorial
Aprendizaje automático de datos categóricos con el tutorial de Python
tutorial