1. Introducción

La importancia del procesamiento paralelo en Python

Python es un lenguaje de programación simple y fácil de usar que se utiliza en una amplia gama de aplicaciones. Sin embargo, cuando se requiere procesamiento de datos o cálculos complejos, la velocidad de ejecución de Python a veces supone un desafío. Para resolverlo, el «procesamiento paralelo», que permite ejecutar varias tareas al mismo tiempo, desempeña un papel importante. En este artículo veremos cómo implementar el procesamiento paralelo en Python, desde los métodos básicos hasta casos de uso concretos.

2. Métodos de procesamiento paralelo en Python

Principales métodos de procesamiento paralelo

Python ofrece varias formas de implementar el procesamiento paralelo. Las principales son las siguientes tres.
  1. Multihilo (módulo threading) Usa varios hilos para ejecutar tareas de forma concurrente, pero, debido al GIL (Global Interpreter Lock) de Python, su efectividad es limitada en tareas intensivas en CPU.
  2. Multiproceso (módulo multiprocessing) Cada proceso dispone de un espacio de memoria independiente, por lo que no se ve afectado por el GIL y permite un paralelismo real. Es adecuado para el procesamiento de datos a gran escala y cálculos pesados.
  3. Procesamiento asíncrono (módulo asyncio) El procesamiento asíncrono es eficaz para tareas de E/S (como comunicaciones de red y operaciones con archivos). De este modo, se pueden llevar a cabo de forma eficiente operaciones con muchos tiempos de espera.

3. Multiproceso vs Multihilo

Impacto del GIL (Global Interpreter Lock)

Python tiene un mecanismo llamado GIL, por el cual solo un hilo puede ejecutarse simultáneamente. Esto provoca que, en tareas intensivas en CPU, aumentar los hilos no mejore el rendimiento. Por ello, el procesamiento paralelo con hilos solo resulta efectivo para tareas de E/S (I/O) con mucho tiempo de espera.

Ventajas y limitaciones del multihilo

Los hilos son ligeros y resultan ideales para tareas de E/S (como operaciones de archivos o comunicaciones de red). Sin embargo, debido al GIL mencionado, no pueden aprovechar por completo varios núcleos de CPU, por lo que no son adecuados para tareas intensivas en CPU.
import threading
import time

def worker(num):
print(f"Worker {num} starting")
time.sleep(2)
print(f"Worker {num} finished")

threads = []
for i in range(5):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()

for t in threads:
t.join()
Este código ejecuta cinco hilos de forma simultánea y cada uno termina tras dormir 2 segundos. Al usar multihilo, se puede observar cómo las tareas avanzan de forma concurrente.

Ventajas del multiproceso

Para sortear las limitaciones del GIL, el multiproceso es una opción eficaz. Los procesos, a diferencia de los hilos, disponen de espacios de memoria independientes, lo que permite aprovechar plenamente múltiples núcleos de CPU. Resulta especialmente útil en cálculos intensivos y cuando se manejan grandes volúmenes de datos.
from multiprocessing import Process
import time

def worker(num):
print(f"Worker {num} starting")
time.sleep(2)
print(f"Worker {num} finished")

if name == 'main':
processes = []
for i in range(5):
p = Process(target=worker, args=(i,))
processes.append(p)
p.start()

for p in processes:
    p.join()
En este ejemplo, cinco procesos se ejecutan de forma concurrente y cada uno realiza su tarea de manera independiente. El método join() espera a que termine cada proceso, por lo que el programa no continúa hasta que se hayan completado todos los procesos.

4. Cómo implementar el procesamiento en paralelo en Python

Procesamiento en paralelo con el módulo multiprocessing

Con el módulo multiprocessing se pueden gestionar varios procesos de forma eficiente. A continuación se muestra un ejemplo básico que usa un pool de procesos para procesar tareas en paralelo.
from multiprocessing import Pool

def square(x):
return x * x

if name == 'main':
with Pool(4) as p:
result = p.map(square, [1, 2, 3, 4, 5])
print(result)
En este código, cuatro procesos se ejecutan simultáneamente y cada uno realiza el cálculo del cuadrado para los elementos de la lista. El resultado se devuelve como una lista, lo que permite verificar la eficiencia del procesamiento en paralelo.

5. Procesamiento asíncrono y sus usos

Procesamiento asíncrono con el módulo asyncio

asyncio es especialmente adecuado para tareas en las que se producen tiempos de espera de E/S. Operaciones como la comunicación de red y la entrada/salida de archivos pueden realizarse con mayor eficiencia al procesar en paralelo otras tareas durante los tiempos de espera.
import asyncio

async def worker(num):
print(f'Worker {num} starting')
await asyncio.sleep(1)
print(f'Worker {num} finished')

async def main():
tasks = [worker(i) for i in range(5)]
await asyncio.gather(*tasks)

asyncio.run(main())
Este código procesa cinco tareas en paralelo. Al usar await se realiza el procesamiento asíncrono y, durante el tiempo de espera de cada tarea, se ejecutan otras tareas.

6. Optimización del rendimiento del procesamiento en paralelo

Paralelización con Joblib

Joblib es una biblioteca para optimizar cálculos pesados, como el procesamiento de datos y el entrenamiento de modelos de aprendizaje automático. El siguiente código es un ejemplo de cómo realizar procesamiento en paralelo con Joblib.
from joblib import Parallel, delayed

def heavy_task(n):
return n ** 2

results = Parallel(n_jobs=4)(delayed(heavy_task)(i) for i in range(10))
print(results)
Al especificar n_jobs, se puede controlar el número de procesos que se ejecutan simultáneamente. En este ejemplo, los cálculos se realizan en paralelo con 4 procesos y se devuelven los resultados como una lista.

7. Aplicaciones prácticas del procesamiento en paralelo en Python

Procesamiento de datos y web scraping

El procesamiento en paralelo en Python es especialmente eficaz en escenarios en los que se manejan simultáneamente grandes volúmenes de datos, como el procesamiento de datos y el web scraping. Por ejemplo, al rastrear páginas web, el uso de multihilo y procesamiento asíncrono permite enviar varias solicitudes al mismo tiempo, lo que puede reducir significativamente el tiempo de procesamiento. Asimismo, en el entrenamiento de aprendizaje automático y en el preprocesamiento de datos, se puede mejorar el rendimiento aprovechando multiprocessing y Joblib.

8. Resumen

El procesamiento en paralelo es una técnica indispensable para aprovechar al máximo el rendimiento de Python. Al utilizar adecuadamente módulos como threading, multiprocessing, asyncio y Joblib, es posible procesar tareas de manera eficiente en diversos escenarios. Aplica estas técnicas en proyectos reales para mejorar la eficiencia del procesamiento.