Crea Multiples Procesos En Python
Cuando tu programa Python se arrastra como tortuga mientras tu procesador apenas suda, probablemente estés dejando sobre la mesa un montón de potencia sin usar. La mayoría de computadoras modernas tienen múltiples núcleos esperando trabajar, pero el código Python estándar solo usa uno. Ahí es donde Crea Múltiples Procesos en Python: Guía Práctica se convierte en tu mejor aliado para aprovechar todo ese poder dormido.
El módulo multiprocessing de Python esquiva elegantemente el famoso Global Interpreter Lock (GIL), ese guardián que impide que los hilos ejecuten código Python simultáneamente. Al crear procesos separados en lugar de hilos, cada uno obtiene su propio intérprete de Python y puede correr en paralelo real.
En esta guía práctica aprenderás a crear múltiples procesos de manera efectiva, desde los conceptos básicos hasta técnicas avanzadas que transformarán tus programas lentos en máquinas de velocidad.
Por Qué Necesitas Múltiples Procesos
¿Alguna vez has visto a un chef trabajando solo en una cocina enorme con cinco estufas? Así trabaja Python por defecto: un solo trabajador con muchos recursos disponibles.
El paralelismo basado en procesos resuelve este problema dividiendo el trabajo entre múltiples trabajadores independientes. Cada proceso tiene su propio espacio de memoria y su propio intérprete de Python.
A diferencia del threading, donde el GIL limita la ejecución simultánea, los procesos múltiples realmente corren en paralelo. Esto es especialmente valioso para tareas que consumen mucha CPU como procesamiento de imágenes, cálculos matemáticos complejos o análisis de datos masivos.
La diferencia de rendimiento puede ser dramática. Un programa que tarda 10 minutos con un solo proceso puede completarse en 2 minutos con 5 procesos bien implementados.
La Clase Process: Tu Primer Proceso
La forma más básica de crear procesos en Python es usando la clase Process. Su sintaxis es sorprendentemente similar a la de threading, lo que facilita la transición.
Aquí está el ejemplo más simple posible:
from multiprocessing import Process
def saludar(nombre):
print(f'Hola {nombre}, soy un proceso separado')
if __name__ == '__main__':
p = Process(target=saludar, args=('María',))
p.start()
p.join()
Fíjate en el bloque if __name__ == '__main__':. No es decoración, es absolutamente necesario. Sin él, tu programa entrará en un bucle infinito creando procesos que crean más procesos.
💡 Si estás dando tus primeros pasos en inteligencia artificial y te preguntas cómo funcionan realmente los algoritmos que aprenden de los datos, te recomiendo explorar qué es el machine learning y cómo se aplica en la práctica para entender los fundamentos de esta tecnología transformadora.
El método start() inicia el proceso de manera asíncrona. El proceso padre continúa ejecutándose inmediatamente después de llamar a start().
El método join() hace que el proceso padre espere a que el hijo termine. Sin join(), el padre podría terminar antes que el hijo, dejando procesos huérfanos.
Pool: El Poder del Paralelismo de Datos
Cuando necesitas ejecutar la misma función con diferentes datos, Pool es tu mejor amigo. Este objeto crea múltiples procesos automáticamente y distribuye el trabajo entre ellos.
from multiprocessing import Pool
def calcular_cuadrado(numero):
return numero * numero
if __name__ == '__main__':
numeros = [1, 2, 3, 4, 5, 6, 7, 8]
with Pool(4) as pool:
resultados = pool.map(calcular_cuadrado, numeros)
print(resultados) # [1, 4, 9, 16, 25, 36, 49, 64]
El argumento 4 en Pool(4) especifica cuántos procesos trabajadores crear. Una buena regla general es usar el número de núcleos de tu CPU.
El método map() distribuye automáticamente los datos entre los procesos disponibles. Es como tener varios empleados procesando facturas simultáneamente en lugar de uno solo.
¿Necesitas más control? Pool ofrece otros métodos útiles:
| Método | Descripción | Cuándo Usarlo |
|---|---|---|
map() | Aplica función a lista completa | Datos simples, orden importante |
imap() | Versión iteradora de map | Listas grandes, procesar mientras llegan resultados |
apply() | Ejecuta función con argumentos | Una sola tarea, bloqueante |
apply_async() | Versión asíncrona de apply | Una tarea, sin bloquear |
starmap() | Como map pero con múltiples argumentos | Funciones con varios parámetros |
La sintaxis with Pool(4) as pool: garantiza que los procesos se cierren correctamente, incluso si ocurre un error. Siempre úsala.
Comunicación Entre Procesos: Queue y Pipe
Los procesos no comparten memoria como los hilos. Necesitas mecanismos específicos para que múltiples procesos se comuniquen entre sí.
Queue: La Cola Segura
Queue funciona como una cola en el supermercado: primero en entrar, primero en salir. Es thread-safe y process-safe, perfecto para comunicación entre procesos.
💡 Si te has preguntado alguna vez qué tecnologías impulsan las plataformas más grandes del mundo, descubrir qué lenguaje utiliza realmente Facebook en su infraestructura te dará una perspectiva fascinante sobre cómo Python y otros lenguajes coexisten en ecosistemas de alto rendimiento.
from multiprocessing import Process, Queue
def productor(cola, items):
for item in items:
cola.put(item)
print(f'Producido: {item}')
cola.put(None) # Señal de terminación
def consumidor(cola):
while True:
item = cola.get()
if item is None:
break
print(f'Consumido: {item}')
if __name__ == '__main__':
cola = Queue()
items = ['manzana', 'banana', 'naranja']
p1 = Process(target=productor, args=(cola, items))
p2 = Process(target=consumidor, args=(cola,))
p1.start()
p2.start()
p1.join()
p2.join()
El patrón productor-consumidor es extremadamente común cuando trabajas con procesos múltiples. Un proceso genera datos mientras otro los procesa.
Pipe: Comunicación Directa
Pipe crea una conexión bidireccional entre dos procesos. Es más rápido que Queue pero solo funciona entre dos procesos.
from multiprocessing import Process, Pipe
def enviar_mensajes(conn):
conn.send('Mensaje desde el proceso hijo')
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target=enviar_mensajes, args=(child_conn,))
p.start()
print(parent_conn.recv())
p.join()
Cada extremo del Pipe tiene métodos send() y recv(). Simple pero efectivo para comunicación punto a punto.
Compartir Estado: Manager y Value
A veces necesitas que los procesos compartan datos. Python ofrece varias opciones, cada una con sus ventajas.
Manager: Objetos Compartidos Complejos
Manager permite compartir listas, diccionarios y otros objetos entre procesos:
from multiprocessing import Process, Manager
def agregar_a_lista(lista_compartida, valor):
lista_compartida.append(valor)
💡 Si estás dando tus primeros pasos en programación o buscas reforzar conceptos fundamentales, te resultará fascinante explorar [cómo implementar la secuencia de Fibonacci en Python paso a paso](/tutoriales-python/serie-de-fibonacci-en-python/), un ejercicio clásico que te ayudará a dominar bucles, recursividad y optimización de código de manera práctica.
if __name__ == '__main__':
with Manager() as manager:
lista = manager.list()
procesos = []
for i in range(5):
p = Process(target=agregar_a_lista, args=(lista, i))
procesos.append(p)
p.start()
for p in procesos:
p.join()
print(list(lista)) # [0, 1, 2, 3, 4] (orden puede variar)
Los objetos Manager tienen un costo de rendimiento porque cada operación requiere comunicación entre procesos. Úsalos cuando realmente necesites compartir estructuras complejas.
Value y Array: Variables Compartidas Simples
Para tipos de datos simples, Value y Array son más eficientes:
from multiprocessing import Process, Value, Array
def incrementar(contador, arr):
contador.value += 1
for i in range(len(arr)):
arr[i] *= 2
if __name__ == '__main__':
contador = Value('i', 0) # 'i' = entero
numeros = Array('d', [1.0, 2.0, 3.0]) # 'd' = double
procesos = [Process(target=incrementar, args=(contador, numeros))
for _ in range(3)]
for p in procesos:
p.start()
for p in procesos:
p.join()
print(f'Contador: {contador.value}')
print(f'Array: {numeros[:]}')
Nota el uso de .value para acceder al valor real. El tipo se especifica con códigos de un carácter como 'i' (integer), 'd' (double), 'f' (float).
Sincronización: Lock y Semaphore
Cuando múltiples procesos acceden a recursos compartidos, necesitas sincronización para evitar condiciones de carrera.
Lock: El Guardián de Recursos
Un Lock garantiza que solo un proceso acceda a un recurso a la vez:
from multiprocessing import Process, Lock, Value
def incrementar_con_lock(contador, lock, veces):
for _ in range(veces):
with lock:
contador.value += 1
def incrementar_sin_lock(contador, veces):
for _ in range(veces):
contador.value += 1
if __name__ == '__main__':
# Con lock
contador1 = Value('i', 0)
lock = Lock()
procesos = [Process(target=incrementar_con_lock,
args=(contador1, lock, 1000))
for _ in range(10)]
for p in procesos:
p.start()
for p in procesos:
p.join()
print(f'Con lock: {contador1.value}') # 10000
# Sin lock
contador2 = Value('i', 0)
procesos = [Process(target=incrementar_sin_lock,
args=(contador2, 1000))
for _ in range(10)]
for p in procesos:
p.start()
for p in procesos:
p.join()
print(f'Sin lock: {contador2.value}') # Probablemente < 10000
💡 Si estás trabajando con funciones en Python y necesitas modificar variables fuera de su ámbito local, te recomiendo explorar cómo usar la palabra clave global correctamente en Python para evitar errores comunes y escribir código más limpio y mantenible.
Sin Lock, obtendrás resultados impredecibles. Con Lock, el resultado es siempre correcto pero más lento.
Semaphore: Control de Acceso Limitado
Semaphore permite que un número limitado de procesos accedan a un recurso simultáneamente:
from multiprocessing import Process, Semaphore
import time
def usar_recurso(sem, id_proceso):
with sem:
print(f'Proceso {id_proceso} usando recurso')
time.sleep(2)
print(f'Proceso {id_proceso} liberó recurso')
if __name__ == '__main__':
sem = Semaphore(2) # Máximo 2 procesos simultáneos
procesos = [Process(target=usar_recurso, args=(sem, i))
for i in range(5)]
for p in procesos:
p.start()
for p in procesos:
p.join()
Imagina un semáforo como una sala con capacidad limitada. Solo dos procesos pueden estar “dentro” al mismo tiempo.
Métodos de Inicio: spawn, fork y forkserver
Python ofrece tres métodos para crear procesos, cada uno con características diferentes:
spawn: Inicia un proceso completamente nuevo de Python. Es el método más seguro pero también el más lento. Es el predeterminado en Windows y macOS.
fork: Copia el proceso padre completo. Rápido pero puede causar problemas con ciertos recursos. Solo disponible en Unix/Linux.
forkserver: Inicia un proceso servidor que luego crea nuevos procesos. Equilibrio entre seguridad y velocidad. Solo en Unix/Linux.
import multiprocessing as mp
def funcion_ejemplo():
print('Hola desde el proceso')
if __name__ == '__main__':
mp.set_start_method('spawn') # o 'fork' o 'forkserver'
p = mp.Process(target=funcion_ejemplo)
p.start()
p.join()
En general, usa spawn para máxima compatibilidad. Usa fork si necesitas velocidad y estás solo en Linux. Usa forkserver cuando fork cause problemas.
💡 Si estás dando tus primeros pasos en programación, comprender la base es fundamental: te recomiendo explorar esta guía completa sobre los tipos de datos fundamentales en Python para dominar strings, listas, diccionarios y más desde cero.
Casos de Uso Prácticos
Veamos ejemplos reales donde crear múltiples procesos marca la diferencia.
Procesamiento de Imágenes
from multiprocessing import Pool
from PIL import Image
import os
def procesar_imagen(ruta):
img = Image.open(ruta)
img_pequeña = img.resize((100, 100))
nombre = os.path.basename(ruta)
img_pequeña.save(f'thumbnails/{nombre}')
return nombre
if __name__ == '__main__':
imagenes = ['foto1.jpg', 'foto2.jpg', 'foto3.jpg', 'foto4.jpg']
with Pool() as pool:
resultados = pool.map(procesar_imagen, imagenes)
print(f'Procesadas: {resultados}')
Este código procesa múltiples imágenes en paralelo. En un solo núcleo tardaría 4 veces más.
Web Scraping Masivo
from multiprocessing import Pool
import requests
def descargar_pagina(url):
try:
respuesta = requests.get(url, timeout=5)
return len(respuesta.content)
except:
return 0
if __name__ == '__main__':
urls = [
'https://ejemplo1.com',
'https://ejemplo2.com',
'https://ejemplo3.com',
# ... muchas más URLs
]
with Pool(10) as pool:
tamaños = pool.map(descargar_pagina, urls)
print(f'Total descargado: {sum(tamaños)} bytes')
El scraping paralelo es ideal porque pasas más tiempo esperando respuestas de red que procesando.
Cálculos Científicos
from multiprocessing import Pool
import numpy as np
def calcular_estadisticas(datos):
return {
'media': np.mean(datos),
'std': np.std(datos),
'max': np.max(datos)
}
if __name__ == '__main__':
# Simular múltiples conjuntos de datos
conjuntos = [np.random.rand(1000000) for _ in range(8)]
with Pool() as pool:
resultados = pool.map(calcular_estadisticas, conjuntos)
for i, stats in enumerate(resultados):
print(f'Conjunto {i}: {stats}')
Los cálculos numéricos intensivos se benefician enormemente del paralelismo basado en procesos.
💡 Si estás dando tus primeros pasos en programación, entender cómo funcionan los tipos de datos y variables en Python te ayudará a construir una base sólida para desarrollar scripts más complejos y eficientes desde el principio.
Errores Comunes y Cómo Evitarlos
Olvidar el if name == ‘main’
Este es el error número uno. Sin este bloque, especialmente en Windows, crearás procesos infinitos:
# MAL - Causará problemas
from multiprocessing import Process
def trabajar():
print('Trabajando')
p = Process(target=trabajar)
p.start()
# BIEN
if __name__ == '__main__':
p = Process(target=trabajar)
p.start()
p.join()
Intentar Compartir Objetos No Serializables
Los procesos solo pueden compartir objetos que se puedan serializar con pickle:
# MAL - Las funciones lambda no se pueden serializar
with Pool() as pool:
pool.map(lambda x: x*2, [1, 2, 3]) # Error
# BIEN - Función definida normalmente
def duplicar(x):
return x * 2
with Pool() as pool:
pool.map(duplicar, [1, 2, 3])
No Cerrar Pools Correctamente
Siempre cierra tus pools para liberar recursos:
# BIEN - Cierre automático
with Pool() as pool:
resultados = pool.map(funcion, datos)
# TAMBIÉN BIEN - Cierre manual
pool = Pool()
try:
resultados = pool.map(funcion, datos)
finally:
pool.close()
pool.join()
💡 Si estás comenzando en programación o quieres dominar ambos lenguajes simultáneamente, te recomiendo explorar esta guía comparativa entre Python y JavaScript donde descubrirás sus diferencias clave, casos de uso ideales y cómo aprovechar lo mejor de cada uno según tu proyecto.
Optimización y Mejores Prácticas
Elige el número correcto de procesos. Más no siempre es mejor. Usa multiprocessing.cpu_count() como referencia:
import multiprocessing as mp
num_procesos = mp.cpu_count()
print(f'CPUs disponibles: {num_procesos}')
# Para tareas CPU-bound
with Pool(num_procesos) as pool:
pass
# Para tareas I/O-bound, puedes usar más
with Pool(num_procesos * 2) as pool:
pass
Minimiza la comunicación entre procesos. Cada intercambio tiene un costo. Diseña tu código para que los procesos trabajen independientemente.
Usa chunksize con map(). Para listas grandes, especificar el tamaño de fragmento mejora el rendimiento:
datos_grandes = list(range(10000))
# Más eficiente para listas grandes
with Pool() as pool:
resultados = pool.map(funcion, datos_grandes, chunksize=100)
Considera concurrent.futures. Para casos simples, ProcessPoolExecutor ofrece una interfaz más moderna:
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor(max_workers=4) as executor:
resultados = executor.map(funcion, datos)
La biblioteca concurrent.futures proporciona una API de más alto nivel que puede ser más fácil de usar para principiantes.
Monitoreo y Debugging
Depurar múltiples procesos puede ser complicado. Aquí algunas técnicas útiles:
from multiprocessing import Process, current_process
import os
import time
def proceso_con_info(nombre):
proceso = current_process()
print(f'Proceso: {proceso.name}')
print(f'PID: {os.getpid()}')
print(f'PID Padre: {os.getppid()}')
time.sleep(2)
print(f'{nombre} completado')
if __name__ == '__main__':
procesos = []
for i in range(3):
p = Process(target=proceso_con_info,
args=(f'Tarea-{i}',),
name=f'Proceso-{i}')
procesos.append(p)
p.start()
for p in procesos:
p.join()
Para logging en múltiples procesos, usa un Queue centralizado:
from multiprocessing import Process, Queue
import logging
def configurar_logger(cola):
logger = logging.getLogger()
handler = logging.handlers.QueueHandler(cola)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
def trabajador(cola, num):
logger = configurar_logger(cola)
logger.info(f'Trabajador {num} iniciado')
# Tu código aquí
logger.info(f'Trabajador {num} finalizado')
Ahora tienes las herramientas para crear múltiples procesos en Python de manera efectiva. Desde la clase básica Process hasta pools avanzados, comunicación entre procesos y sincronización, estás preparado para aprovechar todo el poder de tu hardware. Recuerda que el multiprocesamiento brilla en tareas CPU-intensivas, mientras que para operaciones I/O el threading o asyncio pueden ser mejores opciones. Experimenta, mide el rendimiento y elige la herramienta correcta para cada trabajo.