Guzmán D. Darío Senior Python Developer Español Hire me
Taller autoguiado

De bloqueante a tiempo real: un pipeline de video multi-cámara con asyncio

Paso 09 de 23
Paso 9 de 23

El executor, y el precio que trae

Tienes el event loop congelado y sabes por qué. La salida evidente es sacar ese cálculo a un hilo aparte, y asyncio trae una herramienta justo para eso. Vamos a usarla, a comprobar que funciona, y después a mirar con lupa lo que acabamos de construir.

En la misma rama step-05-cpu-trap hay un segundo archivo, infer_in_executor.py. La diferencia con el anterior cabe en una línea:

infer_in_executor.py python
async def main():
    loop = asyncio.get_running_loop()
    asyncio.create_task(heartbeat())
    await asyncio.sleep(0.35)
    with ThreadPoolExecutor(max_workers=2) as pool:
        for _ in range(4):
            await loop.run_in_executor(pool, detect_in_process, b"")
    await asyncio.sleep(0.35)

loop.run_in_executor(pool, funcion, argumento) manda esa función a un hilo del pool y te devuelve algo que sí puedes esperar con await. El cálculo sigue ocurriendo, pero fuera del hilo del event loop, así que el bucle queda libre para atender a los demás.

Ejecútalo y compara con lo que viste antes:

terminal 2 bash
python infer_in_executor.py
salida text
tick   gap    101 ms
tick   gap    101 ms
tick   gap    101 ms
tick   gap    101 ms
tick   gap    101 ms

Checkpoint · Plano

Ni un hueco. El latido se mantiene en 101 milisegundos de principio a fin, mientras las cuatro detecciones ocurren igual que antes. Acabas de aprender la herramienta estándar para meter código bloqueante en un programa asíncrono.

run_in_executor es la respuesta correcta muchas veces, y en el paso 17 la vas a usar de verdad. Pero ahora viene la parte interesante.

Mira bien lo que acabamos de construir

Da un paso atrás y describe la arquitectura que tienes ahora mismo:

Eso es un pool de hilos. Con ceremonia adicional.

Si tu diseño entero consiste en mandar todo el trabajo a un executor, entonces no necesitas asyncio: threading con una queue.Queue hace lo mismo, en menos líneas y con menos conceptos nuevos que aprender. Sería deshonesto que este taller te vendiera lo contrario.

Ojo

Esta es la pregunta que deberías hacerte en cualquier proyecto asíncrono real: ¿mis tareas se pasan el tiempo esperando, o calculando? Si es lo segundo y no puedes cambiarlo, asyncio te está complicando la vida sin darte nada a cambio.

La pregunta que sí lleva a algún sitio

En vez de preguntarnos cómo meter el cálculo en nuestro programa sin romperlo, demos la vuelta a la pregunta:

¿Y si el cálculo no viviera en nuestro programa?

Porque piénsalo: ¿por qué tiene que correr el modelo dentro de tu proceso de Python? Un modelo de detección es un servicio: recibe una imagen, devuelve unas cajas. Podría estar en otro proceso. En otra máquina. En una máquina con GPU mientras la tuya no la tiene.

Y si el modelo está detrás de la red, entonces desde el punto de vista de tu programa la inferencia deja de ser cálculo y se convierte en espera. Que es justo aquello en lo que asyncio es imbatible.

Analogía · La cocina no lava sus platos

En el paso anterior el mesero se atascaba lavando la vajilla. La solución del executor es contratar a un lavaplatos y que el mesero le pase la loza: funciona, pero ahora tienes que coordinar a dos personas dentro del mismo local, y el mesero se pasa el día haciendo de mensajero.

La otra solución es mandar la vajilla a un servicio de lavado externo. El mesero deja la caja en la puerta y sigue atendiendo mesas. No coordina a nadie: solo espera a que devuelvan la caja limpia, y esperar es exactamente lo que sabe hacer bien.

Checkpoint

En este punto deberías tener dos cosas claras:

  • run_in_executor arregla el síntoma y a veces es la respuesta correcta.
  • Si todo tu trabajo acaba en el executor, lo que construiste es un pool de hilos y asyncio te sobra.

En el siguiente paso el modelo sale de tu proceso, y el pipeline se vuelve puro I/O.