Concurrencia en la cocina de medianoche
·
Resumen del curso
1
La cola de la primera noche: concurrencia y paralelismo
2
Contratar más cocineros: lo básico de threading
3
La pizarra no cuadra: condiciones de carrera y Lock
4
Solo tres fogones: herramientas de sincronización
5
Cuchillos y tablas: interbloqueo
6
El riel de comandas: queue
7
Cocineros de agencia: concurrent.futures
8
Por qué la masa no va más rápido: el GIL y el paralelismo
9
Una sola encargada de sala: primeros pasos con asyncio
10
Se cae la reserva de grupo: concurrencia estructurada
11
La sala se congela: llamadas bloqueantes y combinar herramientas
12
La noche de la crítica: todo junto
Tiempo 2:30–3:20
La comanda de esta noche
#04 · Cuarta noche
Problema
Por un grupo grande, ya son seis cocineros. Pero solo hay tres fogones, y nadie puede encender el fuego hasta que la chef grite "¡Abrimos!".
Objetivo
Nadie enciende el fuego antes de la señal de apertura, se usan como mucho tres fogones a la vez, y los seis pedidos salen en menos de 1 segundo.
Restaurante nocturno Dongsi
Cuarta noche, diez menos veinte. Hay una reserva de grupo, así que han venido tres cocineros más solo por hoy. Seis cocineros, tres fogones.
Minho es el primero en plantarse delante de un fogón y encenderlo. «Si lo caliento antes, voy más rápido.» La chef, sin decir nada, cierra la llave del gas. «Prohibido encender antes de abrir. Y con seis personas amontonadas delante de los fogones, aquí no se mueve nadie.»
Sua levanta la mano. «¿Y si colgamos solo tres fichas numeradas delante de los fogones? Solo puede ponerse al fuego quien tenga una.» La chef sonríe por primera vez. «Esa es la primera herramienta de esta noche.»
El Lock de la lección 3 era una tiza: una persona a la vez. Semaphore(n) son n fichas numeradas. Pueden entrar hasta n personas a la vez, y la persona número n+1 espera a que alguien devuelva su ficha.
import threading
import time
stoves = threading.Semaphore(3) # tres fogones
start = time.perf_counter()
def cook(name):
with stoves: # espera a que quede un fogón libre y lo toma
print(f"{time.perf_counter() - start:.1f}s {name} on")
time.sleep(0.3)
names = [f"cook{i}" for i in range(1, 7)]
threads = [threading.Thread(target=cook, args=(n,)) for n in names]
for th in threads:
th.start()
for th in threads:
th.join()
print(f"total {time.perf_counter() - start:.2f}s")
# por ejemplo:
# 0.0s cook1 on
# 0.0s cook2 on
# 0.0s cook3 on
# 0.3s cook4 on
# 0.3s cook5 on
# 0.3s cook6 on
# total 0.60swith stoves: se toma una ficha (acquire) y al salir se devuelve (release). Si quedan 0 fichas, quien quiere entrar espera.Semaphore(1) deja pasar a una persona a la vez, como un Lock. La diferencia: si sueltas un Lock que ya está suelto, salta RuntimeError; en cambio, un semáforo acepta más llamadas a release() y simplemente suma fichas en silencio (ver BoundedSemaphore más abajo).Cambia el número de fogones en la línea de tiempo de abajo. Las barras amarillas son el tiempo esperando un fogón; las naranjas, el tiempo cocinando de verdad.
Línea de tiempo de la cocina
Pedidos
6
Cocineros
3
Fogones
2
Cocinando
Esperando
Cancelado
Falló
Semaphore no lleva la cuenta de cuántas fichas le devuelven. Si un error hace que llames a release() una vez de más, las fichas pasan a ser cuatro sin que nadie se entere. Hay tres fogones, pero se ponen cuatro al fuego.
loose = threading.Semaphore(3)
loose.release() # se soltó una de más por error
print(sum(loose.acquire(blocking=False) for _ in range(10))) # ahora hay cuatro fogones
# 4
strict = threading.BoundedSemaphore(3)
strict.release()
# ValueError: Semaphore released too many timesBoundedSemaphore lanza ValueError si le devuelven más fichas de las fijadas al principio. Si usas un semáforo para limitar una cantidad, BoundedSemaphore es casi siempre la mejor opción: el error no pasa desapercibido, revienta en el acto.
El consejo de la chef
El semáforo cuenta, no protege la pizarra. Aunque haya tres personas en tres fogones, si las tres escriben en la misma pizarra, otra vez tienes una condición de carrera. Si dentro de una sala donde caben tres tocas un valor compartido, ahí dentro vuelves a usar un Lock.
Un Event es una luz de señal en la pared de la cocina. Empieza apagado, y cuando alguien lo enciende (set()), todos los que esperaban esa señal (wait()) se ponen en marcha a la vez.
opened = threading.Event() # empieza apagado
start = time.perf_counter()
def cook(name):
opened.wait() # espera a que se encienda la señal
print(f"{time.perf_counter() - start:.1f}s {name} fire")
threads = [threading.Thread(target=cook, args=(n,)) for n in ["minho", "sua", "tak"]]
for th in threads:
th.start()
time.sleep(0.5)
print(opened.is_set())
opened.set() # «¡Abrimos!»
for th in threads:
th.join()
print(opened.is_set())
# False
# 0.5s minho fire (el orden de estas tres líneas cambia en cada ejecución)
# 0.5s sua fire
# 0.5s tak fire
# TrueTambién puedes ponerle un límite de tiempo a wait(). En ese caso, importa lo que devuelve: True si la señal se encendió, False si se acabó el tiempo y sigue apagada.
closing = threading.Event()
print(closing.wait(timeout=0.2)) # esperó 0,2 s y no hubo señal
closing.set()
print(closing.wait(timeout=0.2)) # si ya está encendida, True al instante
closing.clear() # se apaga otra vez
print(closing.is_set())
# False
# True
# False| Método | Qué hace |
|---|---|
set() | Enciende la señal. Se despiertan todos los hilos que esperaban |
wait() | Espera a que se encienda. Si ya está encendida, pasa directamente |
wait(timeout) | Espera solo ese tiempo. True si se encendió, si no False |
clear() | Vuelve a apagar la señal. Los wait() siguientes vuelven a esperar |
is_set() | Si está encendida ahora mismo |
Hay un patrón muy habitual: crear la señal de «fin del servicio» con un Event y hacer que el hilo trabajador lo compruebe en cada vuelta del bucle con if closing.is_set(): break. A diferencia del daemon de la lección 2, que se corta a medias, así el hilo recoge por sí mismo y termina.
La línea de tiempo de abajo es una cocina en la que la chef grita «¡Abrimos!» a los 0,5 segundos.
Línea de tiempo de la cocina
Cocineros
3
Cocinando
Esperando
Cancelado
Falló
set() despierta a todos los que esperan.Un Event solo tiene dos estados: encendido o apagado. Pero las esperas de una cocina suelen ser más concretas. Minho espera huevos; Sua, tofu. Cada vez que llega algo a la estantería hay que comprobar «¿ha llegado lo mío?» y, si no, volver a esperar. Para eso está Condition.
Condition es un cerrojo + una lista de «despiértame». Protege el estado compartido (la estantería del género) con el cerrojo y, cuando ese estado cambia, despierta a la gente de la lista.
pantry = [] # estantería del género (estado compartido)
arrived = threading.Condition() # cerrojo de la estantería + lista de «despiértame»
start = time.perf_counter()
def cook(name, need):
with arrived:
ok = arrived.wait_for(lambda: need in pantry, timeout=0.5)
print(f"{time.perf_counter() - start:.1f}s {name} {need if ok else 'gave up'}")
def supplier():
for item in ["tofu", "egg"]: # el pescado hoy no llega
time.sleep(0.1)
with arrived:
pantry.append(item)
arrived.notify_all() # despertar a todos los que esperan
threads = [threading.Thread(target=cook, args=a) for a in [("minho", "egg"), ("sua", "tofu"), ("tak", "fish")]]
threads.append(threading.Thread(target=supplier))
for th in threads:
th.start()
for th in threads:
th.join()
# 0.1s sua tofu
# 0.2s minho egg
# 0.5s tak gave upwith arrived:. La misma regla de la lección 3.wait_for(condición) espera hasta que la condición sea verdadera. Mientras espera, suelta el cerrojo un momento, y así el proveedor puede dejar género en la estantería. Al despertar, vuelve a tomar el cerrojo y comprueba otra vez la condición.timeout, cuando se acaba el tiempo devuelve el último valor de la condición. El pescado no llegó nunca, así que el señor Tak recibió False a los 0,5 segundos y se rindió.notify_all() despierta a todos los de la lista. Cada uno comprueba su propia condición y, si no se cumple, vuelve a dormir. Cuando llegó el tofu, Minho y el señor Tak también se despertaron, pero como no era lo suyo, siguieron esperando.notify() despierta solo a uno de los que esperan. Va bien para trabajos en los que da igual quién se lo lleve, por ejemplo «que cualquier camarero se lleve un plato terminado».
plates = 0
counter = threading.Condition()
def cook():
global plates
time.sleep(0.1)
with counter:
plates += 1
counter.notify() # despertar solo a uno
def waiter(name):
global plates
with counter:
counter.wait_for(lambda: plates > 0)
plates -= 1
print(name, "took a plate")
ws = [threading.Thread(target=waiter, args=(n,)) for n in ["w1", "w2"]]
cs = [threading.Thread(target=cook) for _ in range(2)]
for th in ws + cs:
th.start()
for th in ws + cs:
th.join()
print(plates)
# por ejemplo:
# w1 took a plate
# w2 took a plate
# 0Como por cada plato solo se despierta a uno, hay menos casos en que dos se despiertan a la vez y se pelean por el mismo plato. En cambio, si cada persona espera una condición distinta, notify() puede despertar a quien no toca, así que usa notify_all(). Si dudas, notify_all() es lo seguro.
El consejo de la chef
Cuando uses wait() con un Condition, vuelve a comprobar la condición siempre dentro de un while. Que te despierten no garantiza que haya llegado lo tuyo. Puede ser el género de otro, o puede que alguien se lo haya llevado antes. Con wait_for(condición), ese while lo hace por ti, así que es lo más cómodo.
En una mesa de cuatro hay que servir los cuatro platos a la vez. Si sale uno antes, los otros tres comensales esperan sentados mientras se enfría. Barrier(n) espera en el pase hasta que lleguen los n, y cuando están todos los deja salir juntos.
start = time.perf_counter()
table = threading.Barrier(4, action=lambda: print("serve 4 plates"))
done = []
def cook(name, sec):
time.sleep(sec) # cada uno a su ritmo
i = table.wait() # esperar en el pase hasta que lleguen los cuatro
done.append((round(time.perf_counter() - start, 1), i, name))
crew = [("minho", 0.1), ("sua", 0.2), ("chef", 0.3), ("tak", 0.4)]
threads = [threading.Thread(target=cook, args=c) for c in crew]
for th in threads:
th.start()
for th in threads:
th.join()
print(sorted(done))
# serve 4 plates
# [(0.4, 0, 'minho'), (0.4, 1, 'sua'), (0.4, 2, 'chef'), (0.4, 3, 'tak')]wait() devuelve a cada uno un número distinto, de 0 a n-1. En CPython es el orden de llegada: 0, 1, 2, 3. Con ese número puedes decidir «lo que hace solo uno» (por ejemplo, escribir un log con if i == 0:).action= se ejecuta una sola vez, cuando llega el último. Es la señal para sacar los platos.wait(timeout=) se pasa de tiempo, el Barrier se rompe y todos los que esperaban reciben BrokenBarrierError.| Herramienta | En la cocina | Cuándo |
|---|---|---|
Lock | Una tiza | Para que un valor compartido lo modifique una persona cada vez (lección 3) |
Semaphore(n), BoundedSemaphore(n) | n fichas delante de los fogones | Para que se use como mucho n a la vez |
Event | La luz de la pared | Una señal única para todos, como «empezar» o «terminar» |
Condition | Cerrojo + lista de «despiértame» | «Despiértame cuando se cumpla esto». Para esperar a que cambie el estado compartido |
Barrier(n) | El pase de la mesa de grupo | Salir juntos cuando estén los n |
Las cinco están en threading, y todas se construyen sobre el cerrojo de la lección 3. De hecho, Semaphore, Event y Barrier usan un Condition por dentro, y Condition lleva dentro un cerrojo (un RLock si no le das otro).
El objetivo de esta noche era que nadie encendiera el fuego antes de la señal de apertura, usar como mucho tres fogones a la vez y sacar los seis pedidos en menos de 1 segundo. Con Event marcamos la línea de salida, con Semaphore(3) los fogones y con Lock contamos «cuántos hay ahora al fuego».
opened = threading.Event()
stoves = threading.Semaphore(3)
lock = threading.Lock()
on_fire = peak = 0
log = []
start = time.perf_counter()
def cook(n):
global on_fire, peak
opened.wait() # 1. nada de fuego antes de «¡Abrimos!»
with stoves: # 2. como mucho tres fogones a la vez
with lock:
on_fire += 1
peak = max(peak, on_fire)
time.sleep(0.3)
with lock:
on_fire -= 1
log.append((round(time.perf_counter() - start, 1), f"#{n}"))
threads = [threading.Thread(target=cook, args=(n,)) for n in range(1, 7)]
for th in threads:
th.start()
time.sleep(0.3) # preparativos para recibir a los clientes
opened.set()
for th in threads:
th.join()
print(sorted(log))
print(f"peak {peak}, total {time.perf_counter() - start:.2f}s")
# por ejemplo:
# [(0.6, '#1'), (0.6, '#2'), (0.6, '#3'), (0.9, '#4'), (0.9, '#5'), (0.9, '#6')]
# peak 3, total 0.90speak da 3 todas las veces. Qué números de pedido salen a los 0,6 segundos puede cambiar en cada ejecución.on_fire y peak los modifican los seis cocineros, así que van envueltos en lock. Aunque estemos dentro del semáforo.Hacia el cierre, una clienta sentada sola junto a la ventana termina un cuenco de fideos. Mira una vez su reloj de pulsera, apunta algo en una libretita, paga en silencio y se marcha.
La chef va cerrando las llaves de los fogones una a una. «Hoy todo el mundo ha respetado la cola. Pero ¿y si, además de los fogones, solo hubiera un cuchillo y una tabla? Si Minho tiene el cuchillo y espera la tabla, y Sua tiene la tabla y espera el cuchillo, ¿qué pasa?»
Diario de servicio — Cuarta noche. 6 cocineros, 3 fogones. Espera hasta la señal de apertura (Event) y fogones limitados con tres fichas (Semaphore): 6 pedidos en 0,9 s. Una clienta en la mesa de la ventana, un cuenco de fideos.
closing = threading.Event() y haz que un hilo cocinero prepare un plato cada 0,1 segundos y se detenga cuando closing.is_set(). El hilo principal llama a closing.set() a los 0,35 segundos y luego hace join(). ¿Cuántos platos salieron?Barrier(4) por Barrier(5). Para que no se quede parado para siempre de verdad, cámbialo a table.wait(timeout=1), ejecútalo y comprueba qué excepción salta.La tarjeta de recetas de la chef
Semaphore(n): con with sem:, hasta n a la vez. Para limitar cantidades, BoundedSemaphore(n) atrapa los release de más con ValueError.
Event: al encenderlo con set(), despiertan todos los que hacían wait(). wait(timeout) devuelve True si se encendió y si no, False. clear(), is_set().
Condition: dentro de with cond:, cond.wait_for(condición). Quien cambia el estado llama a notify() (uno) o notify_all() (todos).
Barrier(n): con wait(), cuando se juntan n pasan todos a la vez. Devuelve un número de 0 a n-1; action= se ejecuta una sola vez.
Aunque estés dentro de un semáforo, si modificas un valor compartido, Lock. Las herramientas se combinan.
← Anterior
Lección 3 · La pizarra no cuadra: condiciones de carrera y Lock
Siguiente →
Lección 5 · Cuchillos y tablas: interbloqueo
Recibe avisos de nuevas lecciones
Te avisaremos por correo cuando este curso tenga lecciones nuevas o se abra un curso de continuación.
R
ReflexKR
Aprende Python y Reflex haciendo. Todos los ejemplos se ejecutaron y verificaron en Reflex 0.10.0.
ReflexKR es gratis y sin banners publicitarios. Si te ayudó, puedes apoyarlo con un café. ☕ Invítanos un café
Aviso de afiliados© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Preguntas y respuestas
Pregunta lo que quieras sobre esta lección. Aquí se reúnen las preguntas de todas las versiones de idioma de esta página.