Stack: Django + DRF · Proyecto: TicketFlow Estado: Publicada — impartición tras corregir la 02 Prerrequisito: Lección 02 — Redes básicas
Objetivos
Al terminar esta lección podrás:
- Explicar la diferencia entre proceso, hilo y event loop — y qué modelo usa tu stack en cada capa.
- Demostrar una condición de carrera con código (la misma que puede vender dos veces un asiento).
- Razonar sobre el GIL de Python: qué bloquea y qué no, y por qué Django sigue escalando con hilos.
- Decidir cuándo una vista async (ASGI) aporta y cuándo es ruido.
- Reconocer un deadlock antes de crearlo, con reglas de adquisición de bloqueos.
1. Concurrency no es parallelism (empieza por el vocabulario)
- Concurrencia: el programa gestiona varias tareas a la vez (entrelazándolas). Una cafetera con dos colas y un barista.
- Paralelismo: el programa ejecuta varias tareas al mismo instante físico. Dos baristas.
Un servidor web necesita concurrencia siempre (mientras tu vista espera a la BD, otro request debe poder atenderse) y paralelismo cuando hay CPU real (parsear un CSV grande).
Los tres modelos que existen:
| Modelo | Quién decide el turno | Ejemplo en tu stack |
|---|---|---|
| Procesos | El SO (preemptivo) | workers de Gunicorn (--workers 4) |
| Hilos | El SO (preemptivo) | hilos dentro de cada worker, workers de Celery |
| Event loop | El programa (cooperativo) | vistas async de Django sobre ASGI (Uvicorn), Celery con gevent |
Clave: los modelos se combinan. Gunicorn: 4 procesos × 2 hilos. Uvicorn: 1 proceso con event loop + workers para lo bloqueante. Celery: procesos por concurrencia prefork o hilos con
--pool=threads.
2. El GIL: la regla del juego en CPython
El GIL (Global Interpreter Lock) permite que solo un hilo ejecute bytecode Python a la vez. Consecuencias:
- Dos hilos de Python no paralelizan trabajo de CPU (aunque tengas 8 núcleos).
- Las operaciones de I/O (red, disco, BD) liberan el GIL mientras esperan: por eso los hilos sí sirven para servidores web, cuyo tiempo se va esperando.
- Las extensiones en C (psycopg2, lxml, numpy) liberan el GIL en sus partes pesadas.
Entonces, ¿cómo escala Django si el GIL impide paralelismo por hilo? Con la combinación correcta en cada capa:
Gunicorn --workers 4 ← paralelismo real (4 procesos, GIL por proceso)
hilos/async ← concurrencia barata dentro de cada proceso
BD y Redis fuera ← el estado compartido vive en otro proceso (¡y por eso hay que sincronizar!)Clave de entrevista: "el GIL impide paralelismo de CPU entre hilos, no concurrencia de I/O". Y para CPU: procesos o workers más finos. (CPython 3.13 experimenta con el free-threaded build; en 2026 sigue siendo la excepción, no el estándar de producción.)
3. La condición de carrera: el bug que no se ve en demo
Una carrera ocurre cuando el resultado depende del entrelazado de dos flujos. Patrón clásico check-then-act:
def reserve(seat_id, user):
seat = Seat.objects.get(pk=seat_id)
if seat.is_reserved: # CHECK
raise Conflict("ocupado")
ReservationItem.objects.create( # ACT
seat=seat, reservation=..., price_at_purchase=...)Entre el CHECK y el ACT hay una ventana de milisegundos. Dos peticiones concurrentes pueden ambas leer is_reserved=False y ambas crear su ítem. En tu portátil con un solo usuario nunca pasa; con 500 usuarios el día del concierto, pasa cada hora.
Demostración controlada (la harás en los ejercicios):
import threading
counter = 0
def increment():
global counter
for _ in range(100_000):
counter += 1 # leer, sumar, escribir: tres pasos, no uno
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start(); t2.start(); t1.join(); t2.join()
print(counter) # ¿200_000? Ejecútalo varias veces...counter += 1 no es atómico: son tres operaciones (leer, sumar, escribir) y el planificador puede interrumpir entre ellas. Con dos hilos, el resultado sale por debajo de 200.000 y distinto en cada ejecución. Eso es una carrera: ni determinista ni reproducible con fiabilidad.
Las defensas, de más barata a más fuerte:
- Nada compartido mutable (aísla: una reserva la procesa un solo worker).
- Locks en memoria (
threading.Lock) — solo sirve dentro de un proceso: con 4 workers de Gunicorn no te protege. Error clásico. - Atomicidad en la BD:
updateatómicos, constraints únicas, transacciones con bloqueo (Lección 10). - Colas: serializa el acceso (un job Celery por reserva; el productor-consumidor hace el resto).
Clave: tu invariante I1 de TicketFlow (un asiento, una reserva activa) ya está garantizada por constraints parciales (Lección 00b, ADR-0004). Esta lección te enseña a reconocer todas las ventanas donde el código puede intercalarse, aunque la BD tape las más caras.
4. Deadlocks: el abrazo mortal
Un deadlock necesita cuatro condiciones simultáneas (Coffman): exclusión mutua, retención y espera, no expropiación y espera circular. El caso típico en BD:
Transacción A: UPDATE seats WHERE id=1 → luego UPDATE seats WHERE id=2
Transacción B: UPDATE seats WHERE id=2 → luego UPDATE seats WHERE id=1Cada una retiene su primer bloqueo y espera el que la otra retiene: nadie avanza hasta que la BD mata a una (PostgreSQL lanza Deadlock detected y hace rollback de una transacción; tu código debe reintentarla).
Reglas del oficio para no crearlos:
- Orden global de adquisición: todos los recursos (filas, locks) se toman en el mismo orden (por ejemplo, siempre por
idascendente). Rompe la espera circular. - Transacciones cortas: dentro de una transacción, ni red ni llamadas lentas (nunca llamar a la pasarela de pago dentro de la transacción de reserva — lo serio llega en la Lección 54).
- Un solo lock si basta: agrupa la adquisición (lock de grano grueso cuando la contención es baja).
- Timeout + reintento: asume que puede pasar y responde con reintentos idempotentes (Lección 14).
5. Async en Django: qué resuelve y qué no
Django 5 soporta vistas async def servidas por Uvicorn/Daphne (ASGI). El event loop atiende miles de conexiones con un solo hilo si las esperas son asíncronas (await): una vista que espera a la BD con sync_to_async o drivers async no bloquea a las demás.
Aporta cuando: tu vista hace I/O paralelizable (llamar a dos APIs externas a la vez con asyncio.gather, webhooks, agregadores de pago) o tienes muchísimas conexiones lentas (SSE/WebSockets — Lección 16).
No aporta cuando: la vista es CPU (el event loop es un solo hilo: bloquea a todos), o usas librerías síncronas sin envolverlas (un requests.get en una vista async bloquea el event loop entero: peor que sync).
sync (Gunicorn): 1 request → 1 worker/hilo ocupado
async (Uvicorn): 1 request → "await" libre → el loop atiende otrosRegla práctica para TicketFlow: el proyecto vive feliz con Gunicorn sync + Celery. Async entra cuando llegue el streaming de disponibilidad (Lección 16) o las llamadas paralelas a pasarelas. Elegir por necesidad medible, no por moda.
6. Mapa de concurrencia de TicketFlow (cierra el círculo)
| Capa | Modelo | Qué protege |
|---|---|---|
| Gunicorn workers | procesos | paralelismo CPU; crash isolation |
| Hilos dentro del worker | hilos | I/O simultánea por request |
| PostgreSQL | locks + transacciones | invariante I1 (asiento no vendible dos veces) |
| Redis | atómico single-threaded | contadores, rate limits |
| Celery | procesos/hilos por cola | serializar trabajos lentos (pagos, correos) |
Cada capa sincroniza lo suyo. El error de arquitectura es mezclarlas: un lock de Python no protege contra otro proceso; una constraint de BD no acelera una cola; un rate limit en Redis no sustituye una transacción.
Autoevaluación (respóndeme en el chat)
- Con 4 workers de Gunicorn, ¿te protege un
threading.Lockcontra dos peticiones concurrentes que incrementan el mismo contador? ¿Por qué no, y qué usarías? - ¿Por qué el patrón check-then-act falla aunque el CHECK sea el primer statement? ¿Qué parte del sistema garantiza la atomicidad que falta?
- Una vista async llama a
requests.get(...)(síncrono). ¿Qué pasa con el event loop y con las demás peticiones? ¿Cómo se arregla? - Diseña el orden de adquisición correcto para una operación que toca la fila del asiento 1051 y la fila del usuario 7. ¿Qué regla aplicas?
- ¿Cuándo dirías "a TicketFlow le toca ASGI"? Nombra el gatillo concreto del proyecto.
Continúa con los ejercicios. Las solutions.md solo tras intentarlo.