¿Cómo implementas correctamente el patrón productor-consumidor con una cola acotada?
La cola acotada es la clave del patrón: convierte "el productor es más rápido que el consumidor" de un eventual crash por falta de memoria en un backpressure deliberado y visible.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo implementas correctamente el patrón productor-consumidor con una cola acotada?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#El patrón, y por qué "acotada" es la palabra importante
Productor-consumidor desacopla la generación de trabajo de su procesamiento mediante una cola compartida: uno o más hilos productores colocan elementos en ella, uno o más hilos consumidores los retiran, y la cola misma media la entrega. Eso, en general, ya se conoce. La parte que realmente importa, y la que esta pregunta está evaluando, es la palabra "acotada".
Una cola no acotada entre un productor y un consumidor es una fuga de memoria con pasos extra. Si los productores llegan a superar en velocidad a los consumidores —un pico de tráfico, una dependencia downstream lenta, un consumidor que se cayó y está reiniciando— una cola no acotada simplemente sigue absorbiendo elementos, creciendo en silencio hasta que el proceso se queda sin memoria y muere, usualmente en el peor momento posible y a menudo lejos del código que es la causa real. Una cola acotada convierte esa misma situación en algo que puedes ver y controlar: una vez llena, el productor se bloquea (o, según la API, la llamada falla de inmediato). Eso es backpressure: una señal explícita y visible de que el sistema está recibiendo trabajo más rápido de lo que puede procesarlo, en vez de una invisible que solo aparece como un OOM kill horas después.
#Bloquear, no hacer polling activo
Una implementación correcta se apoya en que la cola misma ofrezca operaciones put/get bloqueantes, en vez de un bucle de espera activa. put() debería bloquear al hilo productor cuando la cola está llena, y get() debería bloquear al hilo consumidor cuando la cola está vacía, ambos sin hacer girar la CPU revisando una condición en un bucle. Esto es exactamente lo que te da gratis una cola acotada bien construida: queue.Queue de Python, ArrayBlockingQueue de Java, y los canales con buffer de Go implementan esto correctamente por dentro, usando variables de condición (o el equivalente nativo del lenguaje) en vez de un bucle de espera activa.
Ese último punto vale la pena decirlo explícitamente en una entrevista: no reimplementes esto a mano con una lista simple y un lock manual, salvo que tengas una razón muy específica. Hacer bien la semántica de wait/notify —evitando despertares perdidos, despertares espurios y señales perdidas— es una de las piezas de código concurrente más propensas a errores de escribir correctamente, y la versión de la librería estándar ya tuvo esos bugs encontrados y arreglados.
#El problema del apagado
La parte que se olvida hasta que muerde: ¿cómo sabe un consumidor que no queda más trabajo por venir, en vez de que la cola simplemente esté vacía por el momento? Dos respuestas comunes:
- Sentinel / "poison pill" - el productor empuja un valor marcador especial una vez que el trabajo real se agotó; un consumidor que lo desencola sabe que debe salir. Con múltiples consumidores tomando de una sola cola, se necesita una píldora por consumidor, ya que cada píldora la consume el único consumidor que resulte desencolarla.
- Una bandera o evento de apagado compartido, revisado por los consumidores usando un
getcon timeout en vez de bloquearse para siempre, para que despierten periódicamente y noten la señal de apagado.
El enfoque de sentinel suele ser más simple y portable entre lenguajes, ya que no necesita ningún primitivo de coordinación extra: viaja sobre la misma cola que ya se está usando para el trabajo real.
import queueimport threadingimport timeNUM_CONSUMERS = 3POISON_PILL = object() # sentinel que le dice a un consumidor que se detengadef producer(work_queue, items):for item in items:work_queue.put(item) # se bloquea aquí cuando la cola se llena - backpressurefor _ in range(NUM_CONSUMERS):work_queue.put(POISON_PILL) # exactamente una píldora por consumidordef process(item, worker_id):time.sleep(0.01) # simula trabajo realprint("worker", worker_id, "procesó", item)def consumer(work_queue, worker_id):while True:item = work_queue.get() # se bloquea aquí mientras la cola está vacíaif item is POISON_PILL:work_queue.task_done()breakprocess(item, worker_id)work_queue.task_done()def run():work_queue = queue.Queue(maxsize=100) # acotada - esa es la clave del patrónitems = range(1000)producer_thread = threading.Thread(target=producer, args=(work_queue, items))consumer_threads = [threading.Thread(target=consumer, args=(work_queue, i))for i in range(NUM_CONSUMERS)]producer_thread.start()for t in consumer_threads:t.start()producer_thread.join()for t in consumer_threads:t.join()
Con maxsize=100, una vez que hay 100 elementos sin procesar en la cola, work_queue.put(item) bloquea al hilo productor hasta que un consumidor llama a get() y libera un espacio. Si los consumidores son genuinamente demasiado lentos para la carga de trabajo, el productor simplemente corre al ritmo de los consumidores en vez de acumular memoria sin límite: eso es el patrón funcionando exactamente como se pretende, no un bug que haya que evitar.
#Eligiendo la cota
El tamaño de la cola en sí es un parámetro de ajuste real, no un número arbitrario:
| Cola no acotada | Cola acotada | |
|---|---|---|
| Productor más rápido que el consumidor | la cola crece sin límite hasta agotar la memoria | el productor se bloquea - backpressure visible y controlable |
| Modo de falla | tardío y catastrófico (un OOM kill, a menudo lejos de la causa real) | inmediato y contenido (un estancamiento o latencia agregada) |
| Ajuste | no hay ninguna palanca disponible | el tamaño de la cola se convierte en un trade-off real de capacidad/latencia |
#Ideas clave
- Ser acotada es la característica real: convierte el crecimiento ilimitado de memoria bajo sobrecarga en backpressure explícito, visible y ajustable.
- Usa una implementación de cola con
put/getcorrectamente bloqueantes (queue.Queue,BlockingQueue, canales con buffer) en vez de programar a mano la lógica de wait/notify. - Resuelve el apagado de forma deliberada - una poison pill por consumidor es simple y portable, pero recuerda que se necesita exactamente una píldora por consumidor, no una en total.
- La capacidad de la cola es un parámetro de ajuste real: cotas pequeñas significan backpressure más ajustado y más estancamientos, cotas grandes significan throughput más suave y detección más tardía de un consumidor con dificultades.
🧠 Check your understanding
0/2 · 0/2 answered1. ¿Por qué se prefiere una cola acotada sobre una no acotada en un sistema productor-consumidor?
2. En el patrón de apagado con sentinel/poison-pill con múltiples consumidores, ¿por qué el productor necesita encolar una píldora por consumidor en vez de solo una?