wandres.dev
CORRUTINAS I · el modelo asimétrico

El ciclo: create, resume, yield y los cuatro estados

Crear una corrutina no ejecuta nada, reanudarla la pone a correr y ceder la devuelve a la suspensión. Los cuatro estados que puede tener, el instante exacto en que muere, lo que devuelve el resume final y por qué consultar el estado desde fuera casi siempre significa que el diseño está mal planteado.

⏱ 17 min

El ciclo de vida de una corrutina cabe en tres funciones y cuatro estados, y sin embargo concentra la mayoría de los errores de quien empieza. La causa es casi siempre la misma: suponer que create ejecuta algo, que resume devuelve lo mismo que la función, o que una corrutina terminada se puede volver a usar. Ninguna de las tres cosas es cierta. Esta lección recorre el ciclo completo desde la creación hasta la muerte, fija el instante exacto en que ocurre cada transición y aclara qué contesta coroutine.status en cada momento, incluido el estado que casi nadie sabe explicar.

🎯 Al terminar esta lección sabrás
  • Separar el instante de creación del primer instante de ejecución y saber qué se decide en cada uno.
  • Nombrar los cuatro estados posibles y las transiciones que llevan de uno a otro.
  • Identificar los dos caminos por los que una corrutina muere y qué devuelve el resume que la mata.
  • Usar coroutine.status y coroutine.running con criterio, y reconocer cuándo consultarlos delata un mal diseño.

Crear no es ejecutar

coroutine.create recibe una función de Lua y devuelve un valor de tipo thread. Y eso es todo lo que hace: reserva una pila nueva, anota cuál es la función que habrá que arrancar y devuelve el objeto suspendido. Ni una sola instrucción del cuerpo se ha ejecutado todavía.

local co = coroutine.create(function(...)
  print("esta linea no se ha impreso aun")
  return "listo"
end)

print(type(co), coroutine.status(co))   --> thread   suspended

La consecuencia práctica importa más de lo que parece. Puedes crear cien mil corrutinas en el arranque de tu programa y no pagas ningún coste de ejecución por ellas, solo el de la memoria de sus pilas vacías. Puedes crearlas y no reanudarlas nunca. Y puedes crear una corrutina cuya función lance un error inmediatamente sin que el error aparezca hasta el primer resume, lo que en la práctica significa que la creación nunca falla por culpa del cuerpo.

La primera llamada a resume es la que arranca de verdad: llama a la función con los argumentos que le pasaste después de la corrutina y ejecuta el cuerpo sobre la pila propia hasta que ocurra una de tres cosas. O la función cede con yield, o retorna, o lanza un error. No hay una cuarta posibilidad, y esas tres salidas gobiernan todo el resto de la lección.

💡
El cuerpo debe ser una función de Lua

coroutine.create exige una función, y en la práctica una función de Lua. Una función de C sin soporte de continuaciones puede ser el cuerpo, pero no podrá ceder desde dentro y encontrarás el clásico attempt to yield across a C-call boundary. Desde Lua 5.2 las funciones de la biblioteca estándar que llaman a código de Lua —pcall, los metamétodos, el iterador de for genérico— están preparadas para dejar pasar la cesión, así que ese error es hoy mucho menos frecuente que en 5.1.

Los cuatro estados

coroutine.status devuelve una de cuatro cadenas, y solo tres son fáciles.

Suspendida. Es el estado inicial y el que resulta de cada cesión. La corrutina existe, tiene estado guardado y está esperando un resume. Es el único estado desde el que se puede reanudar.

Corriendo. Es la corrutina que está ejecutando en este preciso instante. Solo puede haber una en todo el estado de Lua, lo que es otra forma de decir que no hay paralelismo. Preguntar el estado de una corrutina y obtener este valor significa necesariamente que se lo estás preguntando a sí misma.

Muerta. Su función terminó, por retorno o por error, y no admite más reanudaciones. La muerte es definitiva: no existe ninguna operación que reviva una corrutina, y si necesitas repetir el trabajo tienes que crear otra.

Normal. Este es el que casi nadie sabe explicar. Una corrutina está en estado normal cuando ha reanudado a otra y está esperando a que aquella le devuelva el control. No está corriendo, porque quien corre es la que ella reanudó; y no está suspendida, porque no cedió, sino que está en mitad de una llamada a resume. Es, literalmente, el estado de un eslabón intermedio de la cadena, y su existencia es la prueba visible de que el modelo asimétrico apila las corrutinas activas.

stateDiagram-v2
[*] --> suspended: create
suspended --> running: resume
running --> suspended: yield
running --> dead: return
running --> dead: error
running --> normal: reanuda a otra corrutina
normal --> running: la otra cede o termina
dead --> [*]
local interna = coroutine.create(function(padre)
  print("interna ve al padre como", coroutine.status(padre))   --> normal
  coroutine.yield()
end)

local externa = coroutine.create(function()
  local yo = coroutine.running()
  print("externa se ve a si misma como", coroutine.status(yo))  --> running
  coroutine.resume(interna, yo)
end)

coroutine.resume(externa)
print(coroutine.status(externa), coroutine.status(interna))     --> dead   suspended

Morir, y qué devuelve el resume que la mata

Una corrutina muere en el instante en que su función principal termina, y ese instante tiene una firma reconocible en el valor de retorno de resume. Recuerda la forma general: resume devuelve un booleano de éxito seguido de valores.

Cuando la corrutina cede, los valores que siguen al booleano son los argumentos de yield. Cuando la corrutina retorna, los valores que siguen al booleano son los que devolvió la función, y la corrutina queda muerta. Desde fuera, las dos situaciones se distinguen consultando el estado después, no por la forma del retorno: true seguido de un valor puede significar tanto cedió esto como terminó devolviendo esto.

local co = coroutine.create(function()
  coroutine.yield("a")
  return "b"
end)

print(coroutine.resume(co), coroutine.status(co))   --> true a  suspended
print(coroutine.resume(co), coroutine.status(co))   --> true b  dead
print(coroutine.resume(co))                         --> false  cannot resume dead coroutine

Esa ambigüedad no es un descuido del diseño, es una consecuencia de que Lua no quiere imponerte un protocolo. Si tu código necesita distinguir el último valor de los intermedios, la solución idiomática no es consultar el estado sino acordar un centinela: que el productor ceda valores mientras los tenga y retorne un valor especial —o simplemente nada— cuando se acabe. El consumidor sabrá que terminó porque recibió el centinela, no porque preguntó.

El otro camino hacia la muerte es el error, que la lección 15.4 trata a fondo. Baste aquí con la parte del ciclo de vida: un error no propagado dentro de la corrutina la mata igual que un retorno, y el resume correspondiente devuelve false y el objeto de error en vez de true y los valores.

🌱

create no ejecuta

Reserva la pila, anota la función y devuelve un hilo suspendido. Crear un millón de corrutinas no ejecuta un millón de cuerpos.

▶️

El primer resume es especial

Es el único que pasa argumentos a la función. Los siguientes se los pasan al yield que dejó la corrutina detenida, que es un sitio completamente distinto.

⚰️

Muerta es para siempre

No hay reinicio ni reciclaje. Reanudar una muerta no lanza un error: devuelve false con el mensaje correspondiente, así que un resume sin comprobar el primer valor la ignora en silencio.

🔗

Normal es el eslabón intermedio

Toda corrutina que aparezca en estado normal es un ancestro en la cadena de reanudaciones que llega hasta la que corre ahora mismo.

Consultar el estado, y por qué casi nunca deberías

Hay tres funciones de consulta. coroutine.status acepta un hilo y devuelve una de las cuatro cadenas. coroutine.running devuelve el hilo que está ejecutando y un booleano que indica si es el principal. coroutine.isyieldable responde si una cesión sería legal en el punto actual, que es lo mismo que preguntar si estamos dentro de una corrutina reanudable.

local function soy_reanudable()
  local yo, es_principal = coroutine.running()
  return coroutine.isyieldable(), es_principal, coroutine.status(yo)
end

Ahora la advertencia de diseño, que es el contenido real de esta sección. Escribir código que consulta el estado de otra corrutina para decidir qué hacer es, en la inmensa mayoría de los casos, un síntoma de que el protocolo entre las dos partes no está definido. Preguntar sigue viva antes de reanudar es redundante, porque resume ya te contesta eso con su primer valor de retorno y además lo hace sin ventana de tiempo entre la pregunta y la acción. Preguntar está suspendida para decidir si reanudar es equivalente. Y consultar el estado de una corrutina que no eres tú y a la que no acabas de reanudar es sencillamente imposible que sea útil, porque si no la reanudaste tú, su estado no depende de ti.

El uso legítimo de estas funciones es la introspección: depuración, trazas, paneles de diagnóstico y bibliotecas que gestionan colecciones de corrutinas ajenas. coroutine.isyieldable sí tiene además un uso corriente y honesto, que es escribir funciones capaces de comportarse de dos maneras según se las llame desde dentro o fuera de una corrutina, típico en bibliotecas de entrada y salida que ceden si pueden y bloquean si no.

El estado que consultas ya es historia

Hay una razón profunda por la que coroutine.status decepciona a quien intenta construir lógica sobre ella, y no es una peculiaridad de Lua sino un principio general de todo sistema con líneas de ejecución múltiples. Consultar el estado de otra entidad produce una respuesta que describe el pasado, no el presente, porque entre el instante en que la respuesta se calcula y el instante en que tu código actúa sobre ella cabe cualquier cosa. En un sistema con hilos reales esto es evidente y todo el mundo lo ha sufrido; en corrutinas se disimula, porque la ausencia de preempción garantiza que nada cambia mientras tú no cedas, y entonces el programador concluye que la consulta es fiable. Lo es, pero de una forma que hace la consulta inútil: si nada puede cambiar el estado sin tu permiso, entonces tú ya sabías cuál era el estado, porque lo determinó tu propia última acción sobre esa corrutina. Preguntar es admitir que perdiste la cuenta. El diseño correcto, en corrutinas y fuera de ellas, es que el estado viaje en las respuestas de las operaciones y no en consultas separadas: resume te dice si funcionó y qué obtuvo, y ese par de datos es toda la verdad que necesitas y la única que no puede quedar obsoleta entre la pregunta y el uso. Aquí Lua no es minimalista por escasez de recursos sino por precisión conceptual: te da cuatro estados para que puedas mirar el sistema, no para que construyas encima. La diferencia entre un observador y un protocolo es exactamente la diferencia entre un programa que puedes razonar y uno que funciona hasta que deja de hacerlo.

📝
Lo esencial

coroutine.create reserva la pila y devuelve el hilo suspendido sin ejecutar nada; el cuerpo arranca en el primer resume. Los cuatro estados son suspendida, corriendo, normal y muerta, donde normal es el de una corrutina que reanudó a otra y espera. Una corrutina muere al retornar su función o al lanzar un error, y la muerte es irreversible. El resume que la mata devuelve true y los valores de retorno, o false y el error; reanudar una muerta devuelve false con cannot resume dead coroutine, sin lanzar nada. Consultar el estado sirve para diagnosticar, no para decidir.

⚔️ Cartografía el ciclo completo
  1. Crea una corrutina y consulta su estado antes del primer resume, después de una cesión y después del retorno. Anota las tres respuestas.
  2. Reproduce el ejemplo de tres niveles y provoca el estado normal. Averigua qué devuelve coroutine.status aplicado al hilo principal desde dentro de una corrutina anidada.
  3. Escribe un consumidor que reanude en bucle hasta que la corrutina muera, comprobando el primer valor de resume. Después reescríbelo consultando el estado antes de reanudar y explica qué línea sobra.
  4. Diseña un protocolo de centinela: el productor cede valores y retorna nil al agotarse. Comprueba que el consumidor no necesita coroutine.status en ningún punto.
  5. Llama a coroutine.isyieldable desde el cuerpo principal, desde dentro de una corrutina y desde una función anidada dentro de esa corrutina. Explica las tres respuestas.