Cargando



Corrutinas en Lua

Te enseñamos todo lo referente a las corrutinas en Lua, desde las funciones que podemos utilizar para su creación, hasta la implementación de las mismas para la resolución de problemas complejos.


abr 21 2015 12:48
Profesional
nov 25 2016 11:52

corrutinas-lua-cover.jpg

 

Una corrutina es similar a un hilo, es una línea de ejecución con su propio stack, sus propias variables locales y su propio puntero para las instrucciones pero con la particularidad que comparte las variables globales y cualquier otro elemento con las demás corrutinas.

 

Pero debemos aclarar que existen diferencias entre los hilos y las corrutinas, la principal diferencia es que un programa que utiliza hilos corre estos de manera concurrente, las corrutinas por otro lado son colaborativas, donde un programa que utiliza corrutinas solo corre una de estas, y la suspensión de estas solo es lograda si se pide de manera explícita.

 

Las corrutinas son sumamente poderosas, veamos entonces todo lo que engloba este concepto y de qué forma las podemos utilizar en nuestros programas.

 

Conceptos básicos


Todas las funciones relacionadas con las corrutinas en Lua se encuentran en la tabla de corrutinas, donde la función create() nos permite crearlas, la misma tiene un argumento simple y es la función con el código que la corrutina correrá, donde el retorno de la misma es un valor del tipo hilo, el cual representa la nueva corrutina. Incluso, el argumento para crear la corrutina a veces es una función anónima como en el siguiente ejemplo:
co = coroutine.create(function () print("Hola Solvetic") end)
Una corrutina puede tener cuatro estados diferentes:
  • suspendida
  • corriendo
  • muerta
  • normal

 

Cuando creamos la misma, esta empieza en el estado suspendido, lo cual significa que la corrutina no corre de manera automática cuando es creada por primera vez. El estado de una corrutina la podemos consultar de la siguiente forma:

print(coroutine.status(co))
Donde para poder a correr nuestra corrutina solo debemos usar la función de resume(), la cual lo que hace internamente es cambiar el estado de la misma de suspendida a corriendo.
coroutine.resume(co)
Si colocamos todo nuestro código junto y agregamos una línea adicional para consultar el estado adicional de nuestra corrutina luego de hacer resume podemos ver todos los estados por los que pasa la misma:
co = coroutine.create(function () print("Hola Solvetic") end)
print(co)
print(coroutine.status(co))
coroutine.resume(co)
print(coroutine.status(co))
Nos vamos a nuestra terminal y ejecutamos nuestro ejemplo, veamos la salida de nuestro programa:
lua corrutinas1.lua
thread: 0x210d880
Suspended
Hola Solvetic
dead
Como podemos ver la primera impresión de la corrutina es el valor del hilo, luego tenemos el estado suspended, y esto está bien ya que este es el primer estado al hacer la creación, luego con resume corremos la corrutina con lo cual nos imprime el mensaje y luego de esto su estado es dead, ya que cumplió su misión.

 

Las corrutinas a primera vista pueden parecer una manera complicada de llamar funciones, sin embargo son mucho más complejas que eso. El poder de las mismas recae en gran parte de la función yield() la cual permite suspender una corrutina que se encuentra ejecutándose para poder resumir su funcionamiento luego, veamos un ejemplo del uso de esta función:

co = coroutine.create(function ()
for i=1,10 do
print("resumiendo corrutina", i)
coroutine.yield()
end
end)
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
Esta función lo que hará es correr hasta el primer yield, y sin importar que tengamos un ciclo for, la misma solo imprimirá de acuerdo a tantos resume tengamos para nuestra corrutina, para finalizar veamos la salida por la terminal:
lua corrutinas1.lua
1
2
3
4
Esta sería la salida por la terminal.

 

Filtros


Uno de los ejemplos más claros que explican las corrutinas es el caso de consumidor y generador de información. Supongamos entonces que tenemos una función que continuamente genera algunos valores de la lectura de un archivo y luego tenemos otra función que lee estos, veamos un ejemplo ilustrativo de cómo podrían lucir estas funciones:
function generador ()
while true do
local x = io.read()
enviar(x)
end
end

function consumidor ()
while true do
local x = recibir()
io.write(x, "\n")
end
end
En este ejemplo tanto el consumidor como el generador corren sin ningún tipo de descanso y podemos detenerlos cuando no haya más información que procesar, sin embargo el problema aquí es la manera de sincronizar las funciones de enviar() y recibir(), ya que cada uno de ellos tiene su propio bucle, y se asume que el otro es un servicio que puede llamarse.

 

Pero con las corrutinas este problema puede solucionarse de manera rápida y sencilla, usando la dupla de funciones resume/yield podemos hacer que nuestras funciones funcionen sin problema. Cuando una corrutina llama a la función yield, no entra en una nueva función sino que retorna una llamada de que está pendiente y que solo puede salir de ese estado al usar resume.

 

De igual forma cuando se llama a resume tampoco empieza una nueva función, este retorna una llamada de espera a yield, resumiendo este proceso es el que necesitamos para sincronizar las funciones de enviar() y recibir(). Aplicando este funcionamiento tendríamos que utilizar recibir() aplicar resume al generador para que genere la nueva información y luego enviar() aplica yield para el consumidor, veamos cómo quedan nuestras funciones con los nuevos cambios:

function recibir ()
local status, valor = coroutine.resume(generador)
return valor
end

function enviar (x)
coroutine.yield(x)
end

gen = coroutine.create(
function ()
while true do
local x = io.read()
enviar(x)
end
end)
Pero todavía podemos mejorar más nuestro programa, y es usando los filtros, los cuales son tareas que funcionan como generadores y consumidores al mismo tiempo haciendo un proceso de transformación de la información bastante interesante.

 

Un filtro puede hacer resume de un generador para obtener nuevos valores y luego aplicar yield para transformar la data para el consumidor. Veamos cómo podemos añadir los filtros de manera sencilla a nuestro ejemplo anterior:

gene = generador()
fil = filter(gene)
consumidor(fil)
Como vemos fue sumamente sencillo, donde además de optimizar nuestro programa ganamos puntos en legibilidad, importante para el mantenimiento en un futuro.

 

Corrutinas como iteradores


Uno de los ejemplos más claros del generador/consumidor son los iteradores presentes en los ciclos recursivos, donde un iterador genera información que será consumida por el cuerpo dentro del ciclo recursivo, por lo que no sería para nada descabellado utilizar corrutinas para escribir estos iteradores, incluso las corrutinas poseen una herramienta especial para esta tarea.

 

Para ilustrar el uso que le podemos dar a las corrutinas, vamos a escribir un iterador para generar las permutaciones de un arreglo dado, es decir, colocar cada elemento de un arreglo en la última posición y voltearlo, para luego recursivamente generar todas las permutaciones de los elementos restantes, veamos cómo sería nuestra función original sin incluir las corrutinas:

function imprimir_resultado (var)
for i = 1, #var do
io.write(var[i], " ")
end
io.write("\n")
end
Ahora lo que hacemos es cambiar por completo este proceso, primero cambiamos el imprimir_resultado() por yield, veamos el cambio:
function permgen (var1, var2)
var2 = var2 or #var1
if var2 <= 1 then
coroutine.yield(var1)
else
Este es un ejemplo ilustrativo para demostrar el funcionamiento de los iteradores, sin embargo Lua nos provee con una función llamada wrap que es similar a create, sin embargo esta no retorna una corrutina, esta retorna una función que al ser llamada resume una corrutina. Entonces para usar wrap solo debemos usar lo siguiente:
function permutaciones (var)
return coroutine.wrap(function () permgen(var) end)
end
Usualmente esta función es mucho más sencilla de utilizar que create, ya que nos da exactamente lo que necesitamos, que es resumir la misma, sin embargo es menos flexible ya que no nos permite verificar el estatus de la corrutina creada con wrap.

 

Las corrutinas en Lua son una herramienta sumamente poderosa para tratar todo lo que respecta a procesos que deben ejecutarse de la mano pero esperando la finalización de aquel que suministra la información, además pudimos ver su uso para solucionar problemas complejos en lo que respecta a procesos de generador/consumidor y además optimizando la construcción de iteradores en nuestros programas.


¿Te ayudó este Tutorial?


1 Comentarios


David Sanz
ago 24 2015 13:00

Muy bueno.

No esperes más y entra en Solvetic
Deja tus comentarios y aprovecha las ventajas de la cuenta de usuario ¡Únete!

X