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 0:50–1:40
La comanda de esta noche
#02 · Segunda noche
Problema
Primer día de Minho, Sua y el señor Tak. Los tres trabajan sin parar, pero nadie sabe quién terminó qué ni cuándo.
Objetivo
Repartir seis pedidos entre tres personas, terminarlos en menos de 0,6 segundos y dejar registrado quién terminó qué y en qué segundo.
Restaurante nocturno Dongsi
Segunda noche, nueve y media. Tres cocineros de línea se atan el delantal. Minho ya tiene el cuchillo en la mano, Sua ordena las comandas una por una y el señor Tak limpia el fogón sin prisa.
A las diez entran los pedidos y cada uno pone su olla al fuego. Pero media hora después, cuando la chef pregunta «¿Quién hizo el udon de la mesa 3?», nadie sabe qué contestar.
La chef da un golpecito al temporizador que lleva al cuello. «Contratar cocineros, hecho. Hoy aprendemos a ponerles una etiqueta con su nombre, a darles trabajo y a esperar a que terminen.»
En Python, un cocinero es un hilo (thread). Los hilos comparten la misma cocina (la memoria) y cada uno avanza con su propio trabajo. Se crean con threading.Thread.
import threading
import time
def cook(name, dish):
print(f"{name}: start {dish}")
time.sleep(0.3) # esperar a que hierva la sopa
print(f"{name}: done {dish}")
minho = threading.Thread(target=cook, args=("minho", "ramen"))
minho.start() # Minho se pone a trabajar
print("chef: next order") # la chef no espera: pasa a lo siguiente
minho.join() # esperar a que Minho termine
print("chef: served")
# minho: start ramen
# chef: next order
# minho: done ramen
# chef: servedtarget= va el trabajo del hilo (una función) y en args=, los argumentos de esa función, en forma de tupla. No llamas a la función, le pasas la función misma: por eso es cook y no cook().Thread(...) no hace nada todavía. Es como haber contratado al cocinero y nada más. Hasta que no llamas a start(), cook no empieza a ejecutarse en el hilo nuevo.start() vuelve enseguida. Así que, mientras Minho cocina el ramen, la chef (el hilo principal, el que arrancó el programa) pasa a la línea siguiente.join() espera a que ese hilo termine. Es «esperar en el pase hasta que Minho entregue el plato».Si le das un plato a cada uno de los tres, queda así:
def cook(name, dish):
time.sleep(0.3)
print(f"{name}: done {dish}")
crew = [("minho", "ramen"), ("sua", "udon"), ("tak", "soba")]
threads = [threading.Thread(target=cook, args=c) for c in crew]
start = time.perf_counter()
for th in threads:
th.start() # primero arrancan los tres
for th in threads:
th.join() # y después se espera a todos
print(f"{time.perf_counter() - start:.2f}s")
# por ejemplo:
# sua: done udon
# minho: done ramen
# tak: done soba
# 0.30sEl orden en que terminan los tres cambia en cada ejecución. Como los tres acaban casi a la vez, a los 0,3 segundos, depende de a quién despierte primero el sistema operativo. En código concurrente, el «orden de la salida» no es una promesa.
No pongas start() y join() en el mismo bucle. Si alternas th.start(); th.join(), cada persona tiene que terminar para que salga la siguiente, y acabas tardando 0,9 segundos.
También puedes ponerle un límite a la espera. join(timeout=segundos) espera solo ese tiempo y vuelve; para saber si el hilo sigue trabajando, usa is_alive().
slow = threading.Thread(target=time.sleep, args=(1.0,))
slow.start()
slow.join(timeout=0.2)
print(slow.is_alive()) # True: todavía está hirviendo
slow.join()
print(slow.is_alive()) # FalseLínea de tiempo de la cocina
Pedidos
6
Cocineros
3
Cocinando
Esperando
Cancelado
Falló
Thread y llaman a start() y join(), igual que el código de arriba.Thread también tiene un método run(). Por el nombre parece «ejecutar», pero si lo llamas directamente caes en una trampa.
def cook(name, dish):
time.sleep(0.3)
print(f"{threading.current_thread().name}: done {dish}")
threads = [threading.Thread(target=cook, args=c) for c in crew]
start = time.perf_counter()
for th in threads:
th.run() # run() en lugar de start()
print(f"{time.perf_counter() - start:.2f}s")
# MainThread: done ramen
# MainThread: done udon
# MainThread: done soba
# 0.90srun() es un método corriente que llama a target. No crea ningún hilo: lo ejecuta todo en orden en el hilo actual. Por eso las tres veces lo hizo MainThread, y tardó 0,3 × 3 = 0,9 segundos.start(). start() crea el hilo y, dentro de él, llama a run().Un hilo solo puede arrancar una vez. Si llamas dos veces a start() sobre el mismo hilo, da error. Para repetir el trabajo, crea un Thread nuevo.
th = threading.Thread(target=time.sleep, args=(0.1,))
th.start()
th.start()
# RuntimeError: threads can only be started onceEl consejo de la chef
args=("ramen") no lleva coma, así que no es una tupla, es solo una cadena. Entonces se pasan por separado las cinco letras r, a, m, e, n, y salta 'cook() takes 1 positional argument but 5 were given'. Si hay un solo argumento, pon siempre la coma: args=("ramen",). Y otra cosa: una excepción dentro de un hilo no le llega a la chef. En pantalla solo sale 'Exception in thread ...' con el traceback, y el programa sigue como si nada. Mira los logs para que no se te escape.
El problema de esta noche era «no sabemos quién lo hizo». A los hilos se les puede poner una etiqueta con su nombre.
def cook(dish):
me = threading.current_thread() # el hilo que ejecuta este código
print(me.name, dish, threading.get_native_id())
time.sleep(0.2)
a = threading.Thread(target=cook, args=("ramen",), name="minho")
b = threading.Thread(target=cook, args=("udon",)) # ¿y si no le damos nombre?
a.start(); b.start()
print([t.name for t in threading.enumerate()])
print(threading.active_count())
a.join(); b.join()
print(threading.current_thread() is threading.main_thread())
print([t.name for t in threading.enumerate()])
# por ejemplo:
# minho ramen 25680
# Thread-1 (cook) udon 38072
# ['MainThread', 'minho', 'Thread-1 (cook)']
# 3
# True
# ['MainThread']| Herramienta | Qué devuelve |
|---|---|
Thread(name="minho") | Le pone nombre al hilo. Si no le das uno, recibe un número y el nombre de la función, como Thread-1 (cook) |
threading.current_thread() | El objeto del hilo que está ejecutando esta línea ahora mismo |
threading.main_thread() | El hilo principal que arrancó el programa (se llama MainThread). En la cocina de hoy, la chef |
threading.enumerate() | La lista de hilos vivos en este momento (incluido el principal) |
threading.active_count() | La longitud de esa lista |
threading.get_native_id() | El número que el sistema operativo le asignó al hilo. Es el mismo que ves en herramientas del sistema como top -H en Linux |
b, que no tiene nombre, se llama Thread-1 y no Thread-2 porque la numeración solo cuenta los hilos sin nombre.get_native_id() cambia en cada ejecución. El número que usa Python internamente es th.ident (o threading.get_ident()), y ambos se fijan solo cuando el hilo ya arrancó.enumerate(). Quien ya se fue a casa no está en la lista.Una función devuelve su resultado con return, pero el hilo no lo recoge. El valor de retorno de la función target simplemente se tira, y join() tampoco devuelve nada.
def make(dish):
time.sleep(0.1)
return f"{dish} ok"
th = threading.Thread(target=make, args=("ramen",))
th.start()
print(th.join()) # join no devuelve nada
# NonePor eso hay que preparar de antemano un recipiente para los resultados y que el hilo los deje ahí. Lo más fácil es asignarle a cada cocinero su propia casilla.
dishes = ["ramen", "udon", "soba"]
results = [None] * len(dishes) # preparar los huecos antes
def cook(i, dish):
results[i] = make(dish) # cada uno escribe solo en su casilla
threads = [threading.Thread(target=cook, args=(i, d)) for i, d in enumerate(dishes)]
for th in threads:
th.start()
for th in threads:
th.join()
print(results)
# ['ramen ok', 'udon ok', 'soba ok']Si prefieres buscar por nombre, usa un dict.
done = {}
def cook2(name, dish):
done[name] = make(dish)
ts = [threading.Thread(target=cook2, args=(n, d)) for n, d in [("minho", "ramen"), ("sua", "udon")]]
for t in ts:
t.start()
for t in ts:
t.join()
print(sorted(done.items()))
# [('minho', 'ramen ok'), ('sua', 'udon ok')]join(). Si los lees antes, puede que alguna casilla siga en None.Future que devuelve ThreadPoolExecutor en la lección 7 lo hace por ti. Con future.result() recibes el plato directamente.Aunque el hilo principal termine, un programa de Python espera a que terminen todos los hilos que no son daemon antes de cerrarse. Los hilos que creas por defecto no son daemon.
def clean():
for i in range(1, 6):
print("cleaning", i)
time.sleep(0.1)
th = threading.Thread(target=clean) # daemon=False (valor por defecto)
th.start()
time.sleep(0.25)
print("lights off")
# cleaning 1
# cleaning 2
# cleaning 3
# lights off
# cleaning 4
# cleaning 5Aunque la chef grite «¡Apago las luces!», el restaurante no cierra hasta que quien está limpiando termine. Ahora añadamos solo daemon=True al mismo código.
th = threading.Thread(target=clean, daemon=True)
th.start()
time.sleep(0.25)
print("lights off", th.daemon)
# cleaning 1
# cleaning 2
# cleaning 3
# lights off Truejoin() aunque sea daemon. join() espera sea daemon o no.start(). Si no lo indicas, hereda el valor del hilo que lo creó; como el hilo principal no es daemon, normalmente es False.Heredar de Thread y cambiar run. Útil cuando quieres que el cocinero lleve consigo su propio estado (el plato que le toca, el resultado). Como vimos en 2-2, start() llama a run() en el hilo nuevo, así que basta con reescribir run().
class Cook(threading.Thread):
def __init__(self, name, dish):
super().__init__(name=name) # primero, siempre el __init__ del padre
self.dish = dish
self.result = None
def run(self): # start() llama a esto en el hilo nuevo
time.sleep(0.1)
self.result = f"{self.dish} ok"
tak = Cook("tak", "soba")
tak.start()
tak.join()
print(tak.name, tak.result)
# tak soba okthreading.Timer: una vez, dentro de n segundos. Como el temporizador de la pasta: un hilo que llama a una función una sola vez cuando pasa el tiempo fijado. Si todavía no ha sonado, lo puedes apagar con cancel().
start = time.perf_counter()
def ring(msg):
print(f"{time.perf_counter() - start:.1f}s", msg)
noodles = threading.Timer(0.5, ring, args=("noodles up",))
noodles.start() # suena una vez dentro de 0,5 s
eggs = threading.Timer(1.0, ring, args=("eggs up",))
eggs.start()
eggs.cancel() # si lo apagas antes de que suene, no pasa nada
noodles.join()
eggs.join()
print("end")
# 0.5s noodles up
# endthreading.local(): el bolsillo del delantal de cada cocinero. Una variable con el mismo nombre que guarda un valor distinto en cada hilo. El bolsillo depende de quién mete la mano.
pocket = threading.local()
def cook(name, n):
pocket.name = name
pocket.count = 0
for _ in range(n):
pocket.count += 1
time.sleep(0.01)
print(pocket.name, pocket.count)
ts = [threading.Thread(target=cook, args=a) for a in [("minho", 3), ("sua", 5)]]
for t in ts:
t.start()
for t in ts:
t.join()
print(hasattr(pocket, "name")) # el bolsillo de la chef (hilo principal) está vacío
# minho 3
# sua 5
# FalseAunque pocket es una sola variable global, el count de Minho y el de Sua no se mezclan. Es una técnica habitual para que cada hilo tenga su propia conexión a la base de datos.
El consejo de la chef
Cuando crees un hilo, lo primero es ponerle nombre. Si a las tantas de la madrugada abres el log y solo ves Thread-7 y Thread-12 por todas partes, no vas a encontrar nunca al culpable. Basta con meter %(threadName)s en el formato de logging y cada línea lleva su nombre.
El objetivo de esta noche era repartir seis pedidos entre tres personas, terminarlos en menos de 0,6 segundos y dejar constancia de quién terminó qué y en qué segundo. Les ponemos nombre (name=) y hacemos que cada uno apunte una línea en un diario común cada vez que termina.
start = time.perf_counter()
log = [] # (instante en que terminó, cocinero, pedido)
def cook(dishes):
me = threading.current_thread().name
for dish in dishes:
time.sleep(0.3)
log.append((round(time.perf_counter() - start, 1), me, dish))
plan = {"minho": ["#1", "#4"], "sua": ["#2", "#5"], "tak": ["#3", "#6"]}
threads = [threading.Thread(target=cook, args=(d,), name=n) for n, d in plan.items()]
for th in threads:
th.start()
for th in threads:
th.join()
for row in sorted(log):
print(*row)
print(f"total {time.perf_counter() - start:.2f}s, {len(log)} orders")
# 0.3 minho #1
# 0.3 sua #2
# 0.3 tak #3
# 0.6 minho #4
# 0.6 sua #5
# 0.6 tak #6
# total 0.60s, 6 ordersargs=(d,) lleva coma.log.append(...) es una operación de lista que se completa de una vez, así que aunque la llamen varios hilos no se pierde ninguna línea. El diario se lee ordenado, después de join().La chef repasa el diario y asiente. «Bien, ahora sí sabemos quién hizo qué. Mañana probamos una cosa más. Si cuatro personas suman lo que hizo cada una a un único número en la pizarra, ¿ese número saldrá bien?»
Diario de servicio — Segunda noche. Primer día de Minho, Sua y el señor Tak. Con etiquetas de nombre, se repartieron los 6 pedidos de dos en dos y terminaron en 0,6 s. Quedó apuntado en el diario quién sacó qué y en qué segundo.
start() y join() dentro del mismo bucle. Predice cuántos segundos tardará y luego mídelo.sleep según el nombre del cocinero). ¿Quién marca el tiempo total? Compáralo con la línea de tiempo cuando solo una fila se alarga.threading.Timer, crea un temporizador de cocina que suene «tres veces, una cada 0,3 segundos». Un Timer suena una sola vez, así que cada vez que suene tendrás que arrancar uno nuevo.La tarjeta de recetas de la chef
th = threading.Thread(target=funcion, args=(arg,), name="minho") → th.start() → th.join()
Si llamas a run() directamente, no hay hilo nuevo: todo corre en orden en el hilo actual. Los hilos se arrancan con start(), y una sola vez.
Quién está trabajando: current_thread().name, main_thread(), enumerate(), get_native_id()
Los hilos no devuelven valores. Guárdalos en casillas de una lista preparada o en claves de un dict, y léelos después de join().
A un hilo con daemon=True no se le espera al terminar el programa. No lo uses para trabajo que deba terminarse.
Heredar de Thread (sobrescribir run), Timer (una vez dentro de n segundos, cancel), local() (un bolsillo propio para cada hilo)
← Anterior
Lección 1 · La cola de la primera noche: concurrencia y paralelismo
Siguiente →
Lección 3 · La pizarra no cuadra: condiciones de carrera y Lock
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.