Después de intentarlo, aquí está el mapa completo. Lo que no hayas sufrido, léelo dos veces.
Ejercicio 1 — La carrera del contador
- Resultados típicos: 100.137, 152.403, 178.922, 199.211... nunca (o casi nunca) 200.000 exactos. Varían porque el planificador interrumpe los hilos en puntos distintos en cada ejecución: la carrera es no determinista, y esa es justamente su maldad (no se reproduce con un breakpoint).
- Con
threading.Lock()alrededor delcounter += 1, el resultado es siempre 200.000: la sección crítica (leer-sumar-escribir) se vuelve atómica respecto a otros hilos. - Con lock, el bucle se ralentiza bastante (adquisición por iteración + contención de dos hilos peleándose por el lock). Nota fina: si agrupas el lock por bloques (1000 incrementos por adquisición), el coste cae — es la idea del lock granularity que reaparece en transacciones.
Ejercicio 2 — GIL: CPU vs I/O
- CPU: dos hilos tardan aproximadamente lo mismo (o más, por el overhead de cambio de contexto) que un hilo con el doble de trabajo. No escaló: el GIL serializa el bytecode.
- I/O (sleep): dos hilos tardan ~1 s en total, no 2. Sí escaló:
sleeplibera el GIL (como libera el socket al esperar a la BD). - "El GIL impide el paralelismo de CPU entre hilos, no la concurrencia de I/O; para CPU, procesos".
Ejercicio 3 — La ventana check-then-act
- Con 20 hilos y sleep de 1 ms se crean varias reservas (típicamente 10-20: casi todos los hilos entran a la vez). Con sleep de 0.01 son más; con 0.0001, menos pero aún >1. La moraleja: la probabilidad de desastre crece con la duración de la ventana y con el número de hilos — exactamente lo que pasa con la carga real.
- (a) El lock en memoria arregla el experimento... pero solo dentro del proceso: con 4 workers de Gunicorn hay 4 "memorias" y el lock no ve a los otros. (b) La operación atómica única en BD es una constraint única (la que ya tienes en
ReservationItem) aplicada dentro de una transacción con bloqueo (Lección 10:select_for_updateo el INSERT fallando conIntegrityError). La memoria del proceso no escala; el estado compartido se defiende donde vive: en la BD.
Ejercicio 4 — I1 con peticiones reales
- Lo esperado: una petición 201 y la otra 409 (o un 500 si la vista no captura la excepción). La constraint de la 00b hizo imposible la corrupción; la pregunta es si tu API responde con la semántica correcta. Un
IntegrityErrorsin capturar se convierte en 500 "bug del servidor" cuando en realidad el servidor funcionó perfectamente: protegió el negocio. - El diff debería incluir algo así:
from django.db import IntegrityError
from rest_framework import status
@api_view(["POST"])
def reserve(request):
...
try:
item = ReservationItem.objects.create(...)
except IntegrityError:
return Response(
{"code": "seat_already_reserved", "detail": "El asiento acaba de reservarse"},
status=status.HTTP_409_CONFLICT,
)
return Response(..., status=status.HTTP_201_CREATED)Detalle senior: el mensaje no dice "error": dice qué pasó en el dominio. Y el 409 de la Lección 01, Ejercicio 2, se materializa aquí — la teoría y el modelo se abrazan.
Ejercicio 5 — Deadlock en miniatura
- Con
op2()en orden inverso y la ventana desleep, el programa se cuelga:op1retiene A y espera B;op2retiene B y espera A. Espera circular: condición 4 de Coffman cumplida. (Sin el sleep, quizá ni lo ves en 100 ejecuciones: los deadlocks son intermitentes por naturaleza — otro regalo para producción.) - Con orden global A → B en todas las funciones, desaparece la espera circular y el programa termina siempre: la única condición rompible baratamente de las cuatro de Coffman es la circular. En la BD real: actualizar filas siempre ordenadas (por ejemplo
ORDER BY ido IDs ascendentes al tocar asientos de una reserva).
Ejercicio 6 — Async o sync
- Sync. La query es I/O pero corto y no paralelizable dentro de la petición; async no aporta nada aquí y complica el stack (drivers, testing).
- Sync. Verificar firma es CPU barato y encolar es una operación rápida (RPUSH en Redis). El trabajo pesado ya se fue a Celery: la vista debe ser rápida, y lo es.
- Async. Tres llamadas externas independientes con
asyncio.gather()bajan la latencia de ~suma a ~la más lenta. Este es el caso de uso de async: I/O externo paralelizable. - Ni sync ni async: así no es. Un CSV de 2 GB en memoria mata al worker y bloquea (async incluido: CPU pura). Respuesta senior: 202 Accepted + job en Celery + descarga cuando esté (lo diseñaste en la Lección 01, Ejercicio 7).
Ejercicio 7 — Mapa de concurrencia
Configuración defendible para 2 vCPU / 4 GB:
Gunicorn --workers 2 --threads 4 # 2 procesos (paralelismo CPU) × 4 hilos (concurrencia I/O)
Celery --concurrency 2 (cola "pagos") # serializa pasarela/webhooks, aislada de la web
Redis # cache + broker (una sola pieza más que vigilar)
PostgreSQL (misma región) # invariante I1 vive aquí, no en PythonJustificación: los workers no deben superar vCPU para trabajo CPU, y los hilos cubren esperas de I/O (la métrica real es la memoria por worker y la latencia p95 bajo carga, no un número mágico). La cola aparte evita que un pico de correos no retrase los pagos. Si tu propuesta difiere con argumentos (p. ej. --workers 4 con hilos 2 por I/O alto), también es correcta: lo que se evalúa es el porqué.
Resumen del profesor
- Concurrency ≠ parallelism; y el GIL no es el monstruo de la historia: solo muerde la CPU.
- Toda ventana check-then-act es una carrera potencial; la defensa donde vive el estado: la BD.
- Orden global de adquisición mata los deadlocks antes de nacer.
- Async cuando hay I/O externo paralelizable; sync cuando no; colas cuando hay que serializar.
Cuando entregues, cerramos la 03 y pasamos a la Lección 04 — Estructuras de datos y algoritmos: el carrito de asientos de TicketFlow como excusa para hablar de complejidad que se nota en producción.