Módulo 1 · Fundamentos que sostienen todo

Lección 03 — Concurrencia y asincronía

Hilos, procesos, event loop, condiciones de carrera y deadlocks — con la cola de compra de TicketFlow.

Publicada
En esta lección
  1. Ejercicio 1 — La carrera del contador
  2. Ejercicio 2 — GIL: CPU vs I/O
  3. Ejercicio 3 — La ventana check-then-act, observada
  4. Ejercicio 4 — Demuestra la I1 con peticiones reales
  5. Ejercicio 5 — Deadlock en miniatura (dos locks con orden inverso)
  6. Ejercicio 6 — Async o sync (razonado)
  7. Ejercicio 7 — Mapa de concurrencia de tu proyecto
  8. Entrega

Los ejercicios 1-3 se hacen en un script suelto (race_lab.py); el 4-6, dentro de tu TicketFlow. No mires solutions.md hasta entregar.

Preparación:

bash
cd ~/dev/ticketflow && source .venv/bin/activate

Ejercicio 1 — La carrera del contador

Copia esto en race_lab.py y ejecútalo 5 veces:

python
import threading

counter = 0

def increment(n):
    global counter
    for _ in range(n):
        counter += 1

t1 = threading.Thread(target=increment, args=(100_000,))
t2 = threading.Thread(target=increment, args=(100_000,))
t1.start(); t2.start(); t1.join(); t2.join()
print(f"esperado 200000, obtenido {counter}")
  1. Anota los 5 resultados. ¿Alguno dio exactamente 200.000? ¿Por qué varían entre ejecuciones?
  2. Arregla la carrera con threading.Lock() (bloquea la sección que lee-suma-escribe). ¿Deja de variar?
  3. ¿En qué se convierte el rendimiento tras el lock? (mide con time.perf_counter()).

Ejercicio 2 — GIL: CPU vs I/O

  1. CPU: dos hilos que cada uno suma en bucle 10 millones de veces. Mide el tiempo total y compáralo con un solo hilo haciendo el doble de trabajo. ¿Escaló?
  2. I/O: dos hilos que cada uno hace time.sleep(1) (simula una llamada a la BD). Mide el total. ¿Escaló?
  3. Escribe en una línea la conclusión que explicarías en una entrevista.

Ejercicio 3 — La ventana check-then-act, observada

  1. Escribe una versión pequeña del patrón con hilos (sin BD):
python
import threading, time

reservas = []
seat_ocupado = False

def reserva(usuario):
    global seat_ocupado
    if not seat_ocupado:            # CHECK
        time.sleep(0.001)           # ← la ventana (simula la query a la BD)
        reservas.append(usuario)    # ACT
        seat_ocupado = True

hilos = [threading.Thread(target=reserva, args=(u,)) for u in range(20)]
for h in hilos: h.start()
for h in hilos: h.join()
print(f"{len(reservas)} reservas para UN asiento")
  1. ¿Cuántas reservas se crean? Ajusta el sleep (0.0001, 0.01): ¿cómo cambia el desastre?
  2. Arregla el experimento de dos formas: (a) lock en memoria; (b) convirtiendo el check+act en una única operación atómica (pista: ¿qué "operación atómica única" sería en una BD real? nómbrala, no hace falta implementarla).

Ejercicio 4 — Demuestra la I1 con peticiones reales

En tu TicketFlow real (la constraint de la 00b ya protege):

  1. Abre dos terminales y lanza en paralelo (mismo asiento libre, dos usuarios):
bash
# terminal 1
curl -s -X POST http://127.0.0.1:8000/api/events/1/reserve/ -H 'Content-Type: application/json' \
  -d '{"seat_id": 5, "user": "ana"}' & \
# terminal 2 (lanza inmediatamente después)
curl -s -X POST http://127.0.0.1:8000/api/events/1/reserve/ -H 'Content-Type: application/json' \
  -d '{"seat_id": 5, "user": "beto"}'
  1. ¿Qué respondió cada una? ¿Hubo algún 500? ¿O una recibió 201 y la otra 409? Si viste un 500 con IntegrityError, eso es un bug de diseño de API (Lección 01): la constraint salvó los datos, pero la vista no sabía traducir el fallo.
  2. Añade el manejo: captura IntegrityError en la vista y responde 409 Conflict con un cuerpo consistente. Pega tu diff.

Ejercicio 5 — Deadlock en miniatura (dos locks con orden inverso)

  1. Ejecuta esta versión "segura" y confirma que termina:
python
import threading

lock_a = threading.Lock()   # fila del asiento
lock_b = threading.Lock()   # fila del usuario

def op1():   # siempre A → B
    with lock_a, lock_b:
        pass

hilos = [threading.Thread(target=op1) for _ in range(10)]
for h in hilos: h.start()
for h in hilos: h.join()
print("sin deadlock")
  1. Crea op2() que tome B → A y lanza 1 hilo de cada en bucle. ¿Se cuelga? (si no se cuelga en 10 s, fuerza la ventana con un time.sleep(0.001) entre adquisiciones).
  2. Arregla el deadlock imponiendo orden global (todas las funciones A → B). ¿Sigue colgándose?

Ejercicio 6 — Async o sync (razonado)

Para cada caso, decide sync o async en Django y justifica en 1-2 líneas:

  1. GET /api/events/ — listado que consulta PostgreSQL.
  2. POST /api/payments/webhook/ — verifica firma y encola el trabajo pesado en Celery.
  3. GET /api/exchange-rates/ — llama a 3 APIs externas de divisas y agrega el resultado.
  4. POST /api/reports/export/ — genera un CSV de 2 GB en memoria y comprime.

Ejercicio 7 — Mapa de concurrencia de tu proyecto

Dibuja (texto) el mapa de la Lección 6 con TU configuración: cuántos workers de Gunicorn usarías para TicketFlow en una VM de 2 vCPU / 4 GB, con qué --threads, y qué cola de Celery aparte. Justifica cada número.


Entrega

Pega resultados y diffs en el chat. Al corregir cierra la 03 y abre la Lección 04 — Estructuras de datos y algoritmos (con el carrito de asientos como ejemplo).