PYTHON · EJECUCIÓN · DISEÑO DE SISTEMAS
Concurrencia
vs. paralelismo.
La diferencia importante no es cuántos threads aparecen en un diagrama. Es si el sistema necesita administrar mejor la espera o hacer más cálculo al mismo tiempo. Esa respuesta cambia el modelo de ejecución, los límites, los fallos y hasta las métricas que importan.
solapamiento ≠ simultaneidad
01 / EL MODELO MENTAL
Dos problemas distintos.
Dos objetivos distintos.
Concurrencia y paralelismo pueden coexistir, pero no son sinónimos. Separarlos evita optimizar el recurso equivocado y ayuda a explicar qué garantía compra cada pieza de arquitectura.
Coordinar muchas tareas que están en progreso.
Una tarea avanza mientras otra espera. Pueden intercalarse en un solo core o correr sobre varios. El objetivo principal es capacidad de respuesta, throughput y buen uso de recursos que de otro modo quedarían ociosos.
Ejecutar trabajo realmente al mismo tiempo.
El problema se divide entre cores, procesos, intérpretes o máquinas. El objetivo es aumentar capacidad de cómputo o reducir la duración de una carga CPU-bound que admite partición.
Pensalo con una API. Mientras espera una consulta a PostgreSQL, puede aceptar otra conexión: eso es concurrencia. Si luego debe comprimir cuatro imágenes usando cuatro cores a la vez, eso es paralelismo. El primer modelo aprovecha la espera; el segundo agrega capacidad de ejecución.
Ninguno garantiza por sí solo que el sistema sea más rápido. Cada tarea concurrente agrega estado vivo; cada worker paralelo agrega coordinación y movimiento de datos. A partir de cierto punto, más unidades de ejecución solo producen colas, context switching y contención.
UNA DISTINCIÓN ÚTILConcurrencia es una propiedad del diseño. Paralelismo es una propiedad de la ejecución.
02 / DIAGNOSTICAR ANTES DE ELEGIR
No empieces por
async vs. threads.
Empezá por el perfil de la carga. Una decisión de ejecución sin mediciones es una hipótesis disfrazada de arquitectura.
- 01
¿Dónde se va el tiempo?
Separá espera externa, cómputo, memoria, serialización, locks y tiempo en cola. Usá perfiles y trazas, no intuición.
- 02
¿Qué resultado querés mover?
Latencia por request, throughput total, capacidad de respuesta, costo por tarea y tiempo de recuperación pueden pedir soluciones opuestas.
- 03
¿Cuánto cuesta coordinar?
Medí tamaño de los mensajes, frecuencia de sincronización, memoria por worker y granularidad. El trabajo debe amortizar ese costo.
- 04
¿Qué pasa cuando se satura?
Definí límites, prioridad, timeout, cancelación, degradación y rechazo. La capacidad finita necesita una conducta finita.
| SEÑAL OBSERVADA | OBJETIVO | MODELO | PUNTO DE PARTIDA EN PYTHON |
|---|---|---|---|
| La mayor parte del tiempo se va esperando red, disco o base de datos | Atender más trabajo sin dejar recursos ociosos | Concurrencia | asyncio; threads si la librería es bloqueante |
| La CPU permanece alta ejecutando Python puro | Reducir el tiempo total del cálculo | Paralelismo | ProcessPoolExecutor; intérpretes en Python 3.14+ |
| Hay esperas y etapas intensivas de cómputo | Aislar cada cuello de botella y controlar el flujo | Híbrido | asyncio en el borde + cola + workers de CPU |
| La tarea es pequeña o necesita compartir estado constantemente | Evitar que la coordinación cueste más que el trabajo | Secuencial primero | Medir antes de agregar executors o coroutines |
03 / EL TOOLBOX DE PYTHON
Elegí por el cuello de botella,
no por familiaridad.
Python ofrece varias primitivas. La mejor es la que hace explícito el modelo de ejecución y contiene su costo operativo.
asyncio
- USALO PARA
- Muchas operaciones de I/O con librerías async.
- COMPRA
- Miles de tareas livianas, cancelación y coordinación explícitas.
- COSTO
- Una llamada bloqueante detiene el event loop completo.
Threads
- USALO PARA
- I/O bloqueante, SDKs sin API async y compatibilidad con código existente.
- COMPRA
- Memoria compartida y una migración incremental sencilla.
- COSTO
- Carreras, locks y, en CPython tradicional, sin speedup para CPU Python puro.
Procesos
- USALO PARA
- Cómputo Python puro que puede dividirse en unidades independientes.
- COMPRA
- Paralelismo multinúcleo y aislamiento de memoria.
- COSTO
- Serialización, memoria adicional, startup y comunicación entre procesos.
Intérpretes
- USALO PARA
- CPU en Python 3.14+ cuando el aislamiento por intérprete encaja.
- COMPRA
- Paralelismo real dentro de un proceso, con un GIL por intérprete.
- COSTO
- Estado mutable aislado y datos serializables entre workers.
CONCURRENCIA ACOTADA
`async` no significa
“sin límites”.
Este ejemplo mantiene como máximo veinte requests en vuelo, aplica un timeout por
operación y agrupa el ciclo de vida de las tareas. Si una tarea falla con un error no
manejado, TaskGroup cancela sus hermanas: esa es una decisión de semántica,
no un detalle de sintaxis.
Semaphoreprotege la dependencia.asyncio.timeoutlimita la espera.TaskGroupevita tareas huérfanas.
import asyncio
import httpx
async def fetch_one(client, url, slots):
async with slots:
try:
async with asyncio.timeout(2.0):
response = await client.get(url)
response.raise_for_status()
return response.json()
except TimeoutError:
return {"url": url, "error": "timeout"}
async def fetch_many(urls):
slots = asyncio.Semaphore(20)
async with httpx.AsyncClient() as client:
async with asyncio.TaskGroup() as group:
tasks = [
group.create_task(fetch_one(client, url, slots))
for url in urls
]
return [task.result() for task in tasks]
Si un SDK bloqueante no ofrece una API async, await asyncio.to_thread(...)
permite sacarlo del event loop. Es una herramienta de compatibilidad muy útil para I/O;
en la build tradicional de CPython no transforma automáticamente código Python
CPU-bound en trabajo multinúcleo.
Para cómputo independiente, un ProcessPoolExecutor evita el GIL usando
procesos separados. La función, argumentos y resultados deben poder serializarse, y el
bloque debe ser lo bastante grande para amortizar ese viaje.
from concurrent.futures import ProcessPoolExecutor
def transform(chunk):
# Trabajo CPU-bound, sin estado compartido.
return expensive_normalization(chunk)
def transform_all(chunks):
with ProcessPoolExecutor() as pool:
return list(pool.map(transform, chunks, chunksize=8))
if __name__ == "__main__":
result = transform_all(load_chunks()) 04 / DEL RUNTIME AL SISTEMA
El patrón que escala
suele ser híbrido.
Una aplicación real mezcla conexiones, cómputo y dependencias lentas. El objetivo no es elegir un ganador: es darle a cada etapa un modelo y un presupuesto propios.
La API no procesa OCR dentro del request. Acepta trabajo, lo hace durable y devuelve un identificador. La cola desacopla ritmos; los workers paralelos consumen según la CPU disponible; el cliente observa progreso sin mantener recursos pesados abiertos.
Bounded concurrency
Si la base admite 40 conexiones, lanzar 400 queries no crea capacidad. Crea una cola dentro del lugar menos observable.
Backpressure
Cuando el consumidor se atrasa, el productor debe esperar, degradar o rechazar. Una cola infinita solo aplaza el incidente.
Bulkheads
Separá pools por tipo de trabajo o prioridad. Un reporte pesado no debería consumir la capacidad del login.
Idempotencia
Timeout no significa fracaso: significa resultado desconocido. Reintentar solo es seguro si repetir no duplica el efecto.
05 / CÓMO FALLA EN PRODUCCIÓN
Más tareas también son
más estados posibles.
La complejidad real aparece en saturación, cancelaciones y resultados parciales. El modelo está incompleto hasta que esos estados tienen una respuesta explícita.
Fan-out sin límite
Crear una tarea por elemento parece eficiente hasta que 50.000 requests agotan sockets, memoria o el pool de la base de datos.
Límite explícito, cola acotada y rechazo o degradación cuando no hay capacidad.
CPU dentro del event loop
Una transformación pesada bloquea todas las conexiones aunque la ruta esté escrita con async def.
Mover la etapa a procesos, intérpretes o una librería nativa que libere el GIL.
Retries sincronizados
Cuando una dependencia vuelve lentamente, todos los workers reintentan juntos y provocan una segunda caída.
Backoff exponencial con jitter, presupuesto de reintentos e idempotencia.
Estado mutable compartido
El resultado depende del orden exacto de ejecución y aparecen carreras difíciles de reproducir.
Mensajes inmutables, ownership claro, locks pequeños o partición por clave.
LAS PREGUNTAS QUE EL CÓDIGO NO RESPONDE SOLO
- TIMEOUT
¿El usuario puede reintentar o el trabajo continúa en background?
- CANCELACIÓN
¿Se propaga a subtareas y workers o solo dejamos de esperar?
- PARCIAL
¿Devolvemos lo disponible, fallamos todo o marcamos cada elemento?
- ORDEN
¿Importa globalmente, por cliente, por documento o no importa?
06 / UN MÉTODO REPETIBLE
Diseñar desde la carga
hasta la operación.
Este recorrido sirve para una API, un crawler, un pipeline de datos o un servicio de procesamiento multimedia.
- 01
Medí la versión simple
Perfilá CPU, I/O, memoria y locks. Registrá p50, p95 y p99, no solo duración media.
- 02
Nombrá la unidad de trabajo
Request, archivo, partición, imagen o cliente. Definí qué estado comparte y qué orden necesita.
- 03
Elegí el modelo por etapa
Async para esperar sin bloquear; threads para I/O bloqueante; procesos o intérpretes para CPU aislable.
- 04
Poné límites donde nace el trabajo
Semáforos, pools y colas acotadas. Si no hay capacidad, la respuesta debe ser visible y deliberada.
- 05
Diseñá el ciclo de vida
Timeout, cancelación, retry, idempotencia, shutdown y recuperación de tareas huérfanas.
- 06
Probá el sistema, no el happy path
Carga sostenida, bursts, dependencia lenta, worker muerto y datos con skew. Observá dónde aparece la primera cola.
DESIGN REVIEW
Antes de aprobar
el diagrama.
- 01Puedo decir si el cuello de botella es espera, CPU, memoria o contención.
- 02Definí si optimizo latencia, throughput, costo o capacidad de respuesta.
- 03Cada recurso finito tiene un límite: conexiones, tareas, cola y workers.
- 04Timeout, cancelación y resultado parcial tienen una semántica de producto.
- 05Los retries son acotados, espaciados e idempotentes.
- 06El orden requerido está definido por entidad; no impongo orden global sin necesidad.
- 07Mido tiempo en cola, tiempo de servicio, tareas activas, saturación y errores.
- 08Probé con carga y fallos; no inferí capacidad desde un ejemplo local.
LA DECISIÓN EN UNA LÍNEA
Espera mejor con concurrencia.
Calculá más con paralelismo.
Y cuando el sistema haga ambas cosas, separá las etapas, acotá el trabajo en vuelo y tratá saturación, cancelación y retries como comportamiento de producto. La arquitectura correcta no es la que ejecuta más tareas: es la que mantiene control cuando llega el pico.
07 / FUENTES Y PRÓXIMOS PASOS
Para seguir
profundizando.
- 01Concurrency vs. ParallelismEl artículo que disparó esta nota · freeCodeCamp ↗
- 02Coroutines and TasksTaskGroup, cancelación y timeouts · Python Docs ↗
- 03concurrent.futuresThreads, procesos e intérpretes · Python Docs ↗
- 04Python support for free threadingBuilds sin GIL y limitaciones actuales · Python Docs ↗