El módulo multiprocessing ejecuta código Python en procesos separados del sistema operativo. Cada proceso posee su propio intérprete y espacio de memoria, lo que permite aprovechar varios núcleos para trabajo limitado por CPU. El módulo también ofrece queues, pipes, locks, memoria compartida, pools, managers y primitivas de sincronización con una API inspirada en threading.
Los procesos no son simplemente threads más rápidas. Cuestan más al iniciar, consumen memoria adicional y requieren serialización cuando los objetos cruzan límites de proceso. Conexiones, sockets, archivos, locks y recursos externos necesitan reglas explícitas de ownership. Un diseño robusto comienza eligiendo un método de inicio apropiado y reduciendo la comunicación entre workers.
Protege el punto de entrada
En plataformas que inician workers importando el módulo principal, la creación de procesos debe estar protegida.
from multiprocessing import Process
def trabajar(valor):
print(valor * 2)
if __name__ == "__main__":
proceso = Process(target=trabajar, args=(21,))
proceso.start()
proceso.join()
Sin la protección, cada hijo importado puede intentar crear nuevos hijos, causando recursión, errores de startup o una tormenta de procesos.
Process, start y join
Process representa un hijo. start() solicita su creación, join() espera y exitcode indica cómo terminó.
proceso.start()
proceso.join(timeout=10)
if proceso.is_alive():
proceso.terminate()
proceso.join()
print(proceso.exitcode)
Un timeout de join() no mata el proceso. El coordinador debe decidir si sigue esperando, solicita cancelación cooperativa o utiliza fuerza como último recurso.
Métodos de inicio
Los métodos principales son spawn, fork y forkserver, con disponibilidad y defaults distintos según la plataforma. spawn lanza un intérprete nuevo, fork copia el proceso actual y forkserver pide a un servidor dedicado que cree hijos.
Selecciona el método de forma explícita cuando forme parte de la arquitectura.
import multiprocessing as mp
if __name__ == "__main__":
contexto = mp.get_context("spawn")
proceso = contexto.Process(target=trabajar, args=(10,))
proceso.start()
proceso.join()
Riesgos de fork
fork hereda memoria, descriptores y estado de bibliotecas. En un programa multithread, locks mantenidos por threads que desaparecen pueden permanecer bloqueados en el hijo. Clientes de base, TLS, logging y runtimes nativos también pueden quedar inconsistentes.
Consulta posix en Python para la capa de procesos Unix. En aplicaciones complejas, spawn suele ser más predecible aunque tenga mayor coste de startup.
Serialización
Los argumentos enviados a workers, mensajes de queues y resultados de pools suelen serializarse. Las funciones de worker deberían definirse a nivel de módulo. Lambdas, closures, generators activos, locks y conexiones abiertas pueden no ser transferibles.
Nunca deserialices payloads de multiprocessing recibidos de una fuente no confiable. La reconstrucción de objetos Python puede ejecutar código.
Queue
Queue ofrece paso de mensajes seguro entre procesos con una interfaz similar a una queue de threads.
from multiprocessing import Process, Queue
def productor(fila):
for numero in range(5):
fila.put(numero * numero)
fila.put(None)
if __name__ == "__main__":
fila = Queue(maxsize=10)
proceso = Process(target=productor, args=(fila,))
proceso.start()
while True:
item = fila.get()
if item is None:
break
print(item)
proceso.join()
Una queue limitada aplica backpressure. Define un protocolo de finalización mediante sentinela, evento, cierre o tipo de mensaje.
Evita deadlocks con join y queues
Un proceso que escribió muchos datos puede esperar que buffers internos sean consumidos antes de salir. Si el padre llama join() antes de leer la queue, ambos lados pueden bloquearse.
Consume mensajes mientras el worker está activo y sigue el orden documentado para cerrar y unir queues.
Pipe
Pipe() crea dos conexiones y sirve para comunicación punto a punto.
from multiprocessing import Pipe, Process
def hijo(conexion):
conexion.send({"status": "ok"})
conexion.close()
if __name__ == "__main__":
padre, punta_hijo = Pipe(duplex=False)
proceso = Process(target=hijo, args=(punta_hijo,))
proceso.start()
punta_hijo.close()
print(padre.recv())
proceso.join()
Cierra extremos no usados. Descriptores adicionales abiertos pueden impedir detectar EOF.
Pool
Pool mantiene un conjunto fijo de workers para muchas tareas.
from multiprocessing import Pool
def cubo(numero):
return numero ** 3
if __name__ == "__main__":
with Pool(processes=4) as pool:
print(pool.map(cubo, range(10)))
Para una API más compacta y uniforme entre threads y procesos, lee concurrent.futures en Python.
map, imap y resultados unordered
map() recopila resultados ordenados y puede retener muchos datos. imap() los entrega gradualmente y imap_unordered() produce elementos concluidos sin conservar el orden de entrada.
Usa chunksize para agrupar entradas. Chunks pequeños aumentan overhead; chunks enormes reducen balanceo y retrasan resultados tempranos.
apply_async y callbacks
apply_async() devuelve un resultado asíncrono y puede recibir callbacks de éxito y error.
with Pool(4) as pool:
resultado = pool.apply_async(cubo, (5,))
print(resultado.get(timeout=5))
Los callbacks se ejecutan en el proceso coordinador y deben ser cortos. Recoge errores explícitamente.
Cierra pools correctamente
close() impide nuevas tareas, join() espera workers y terminate() los detiene abruptamente. Un context manager simplifica cleanup, pero entiende su comportamiento ante excepciones.
No dependas del garbage collector para finalizar pools globales.
Events y cancelación cooperativa
Un Event compartido permite solicitar parada.
from multiprocessing import Event
def worker(detener):
while not detener.is_set():
ejecutar_bloque()
Los workers deben comprobar la señal en intervalos razonables y usar timeouts en operaciones bloqueantes.
terminate y kill
La terminación forzada puede dejar locks adquiridos, queues corruptas, archivos incompletos y transacciones externas abiertas. Úsala solo después de que falle el shutdown cooperativo y con un plan de recuperación.
No termines un worker durante una actualización crítica salvo que el almacenamiento sea transaccional o recuperable.
Locks y semáforos
Lock, RLock, Semaphore, BoundedSemaphore, Condition y Barrier coordinan procesos. Elige la primitiva más simple y mantén corta la sección crítica.
from multiprocessing import Lock, Value
lock = Lock()
contador = Value("i", 0)
with lock:
contador.value += 1
El context manager garantiza liberación ante excepciones.
Value y Array
Value y Array colocan valores compatibles con C en memoria compartida. El acceso puede sincronizarse de forma automática o explícita.
Son útiles para estado pequeño, pero estructuras mutables complejas se vuelven difíciles. Mensajes inmutables por queue suelen ser más seguros.
shared_memory
La API de memoria compartida permite acceder a un bloque de bytes sin copiar grandes arrays mediante serialización. Todos los participantes deben acordar layout, tipo, shape, tamaño y lifetime.
El creador debe cerrar y desvincular el bloque en el momento correcto. Un crash puede dejar recursos huérfanos.
Managers
Un Manager inicia un proceso servidor que expone proxies como listas, diccionarios, locks y namespaces. Es cómodo, pero cada operación necesita comunicación y serialización.
No uses un diccionario administrado dentro de un loop de alta frecuencia. Agrupa cambios o rediseña el flujo con mensajes.
Comparte menos
La arquitectura más fiable suele enviar una tarea autocontenida al worker y recibir un resultado autocontenido. El estado mutable compartido aumenta acoplamiento, sincronización y riesgo de deadlock.
Particiona datos para que cada proceso posea una región.
Inicialización de workers
Los pools pueden ejecutar un initializer dentro de cada worker. Es un buen lugar para abrir una conexión por proceso o cargar recursos read-only. No reutilices ciegamente conexiones creadas en el padre.
conexion = None
def iniciar_worker():
global conexion
conexion = abrir_conexion()
Cierra recursos al salir y no escribas secretos en logs.
Memoria y copy-on-write
En sistemas con fork, las páginas pueden compartirse hasta que un proceso las modifica. Esto puede acelerar startup, pero las escrituras aumentan memoria. La gestión de objetos y el garbage collection pueden tocar páginas inesperadamente.
Mide la memoria residente total de todos los procesos. resource en Python explica varios límites Unix.
Tratamiento de excepciones
Una excepción del worker debe ser observada por el padre. Los pools relanzan fallos cuando el caller invoca get() o consume resultados. Si nunca recoge el resultado, puede perder el error.
Incluye identificador de tarea y conserva traceback.
Logging
Varios procesos escribiendo directamente en un archivo pueden mezclar registros. Usa QueueHandler y un listener central.
Incluye PID, nombre del proceso, tarea, intento y duración.
Signals y shutdown
Haz que el coordinador gestione señales del sistema y distribuya cancelación. Los signal handlers deben ser mínimos. Consulta signal en Python.
Durante shutdown, detén la producción, envía sentinelas, espera un deadline, termina solo workers restantes y cierra queues y memoria compartida.
Ejecutables congelados
Aplicaciones empaquetadas pueden necesitar freeze_support(). Prueba el ejecutable real, no solo el script de desarrollo.
Seguridad
No expongas managers o conexiones de multiprocessing en red sin autenticación y aislamiento. Nunca aceptes payloads serializados de clientes no confiables.
Limita procesos, memoria, profundidad de queues y tamaño de tareas para evitar agotamiento local.
Pruebas
Prueba métodos de inicio diferentes, pools pequeños y grandes, excepciones, crashes, queues llenas, Ctrl+C, shutdown, tareas bloqueadas, datos grandes, sistemas sin fork y ejecutables empaquetados.
Evita assertions de timing exacto. Coordina con eventos y deadlines amplios.
Errores comunes
Los fallos frecuentes son olvidar la protección principal, enviar lambdas, compartir conexiones heredadas, llamar join() antes de consumir queues, usar Manager para todo, terminar durante escrituras, ignorar excepciones, crear demasiados workers y filtrar bloques de memoria compartida.
Conclusión
multiprocessing permite paralelismo real y comunicación rica entre procesos, pero requiere disciplina con serialización, lifecycle, ownership y cleanup. Prefiere tareas independientes, comunicación limitada, cancelación cooperativa y pools controlados.
Consulta la documentación oficial de multiprocessing y compárala con concurrent.futures para una API de tareas más compacta.







