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 deadComo 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 4Esta 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 endEn 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") endAhora 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) elseEste 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) endUsualmente 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.
Muy bueno.