Los pools permiten reutilizar hilos o procesos para ejecutar muchas tareas sin pagar el costo de crearlos y destruirlos cada vez. En Python hay dos familias: multiprocessing.Pool y los ejecutores de concurrent.futures (ThreadPoolExecutor y ProcessPoolExecutor).
Se levantan un número fijo de workers y se reparten trabajos; esto mantiene la latencia baja y evita la sobrecarga de spawn repetido.
multiprocessing.PoolAsyncResult para consultar más tarde.starmap).close() deja de aceptar trabajos; join() espera; terminate() aborta de inmediato.close()+join() para cierres limpios y reserva terminate() para emergencias.concurrent.futures.ThreadPoolExecutordone()), cancelar y componer tareas.as_completed() para consumir resultados en orden de finalización.result(); siempre captura en el código llamador.concurrent.futures.ProcessPoolExecutorThreadPoolExecutor pero con procesos, ideal para CPU-bound.El ejemplo usa Pillow (pip install pillow) para aplicar filtros a una carpeta. Compara una versión secuencial con otra usando ProcessPoolExecutor. Cambia CARPETA y asegúrate de que exista con imágenes soportadas.
El resultado se guarda en imagenes_procesadas; si la carpeta no existe, se crea al vuelo.
import os
import time
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path
from PIL import Image, ImageFilter
CARPETA = Path("imagenes")
SALIDA = Path("imagenes_procesadas")
SALIDA.mkdir(exist_ok=True)
def procesar(path: Path):
with Image.open(path) as img:
blur = img.filter(ImageFilter.GaussianBlur(radius=2))
gray = blur.convert("L")
destino = SALIDA / f"proc_{path.name}"
gray.save(destino)
return path.name, destino.name
def version_secuencial(paths):
inicio = time.perf_counter()
resultados = [procesar(p) for p in paths]
return resultados, time.perf_counter() - inicio
def version_pool(paths, workers):
inicio = time.perf_counter()
with ProcessPoolExecutor(max_workers=workers) as pool:
resultados = list(pool.map(procesar, paths))
return resultados, time.perf_counter() - inicio
if __name__ == "__main__":
paths = [p for p in CARPETA.iterdir() if p.suffix.lower() in {".png", ".jpg", ".jpeg"}]
if not paths:
raise SystemExit("No hay imagenes en la carpeta 'imagenes'")
sec, t_sec = version_secuencial(paths)
pool_res, t_pool = version_pool(paths, os.cpu_count())
print(f"Secuencial: {len(sec)} archivos en {t_sec:.2f}s")
print(f"Pool: {len(pool_res)} archivos en {t_pool:.2f}s con {os.cpu_count()} procesos")
Los pools amortizan el costo de creación de procesos: abren unos pocos al inicio y los reutilizan para cada imagen. En carpetas pequeñas el pool puede no ganar por el overhead de IPC; en lotes grandes de imágenes el paralelismo real suele reducir el tiempo total.