Bienvenidos sean a este post, continuando con el tema del post anterior hoy veremos dos temas que tienen como base a uno de los ejemplos mas paradigmaticos de las corutinas como es el problema de productor-consumidor, vamos a suponer que tenemos una funcion que produce valores (p.e. provenientes de un archivo) y otra funcion que consume continuamente esos valores (p.e. escribiendolos en otro archivo), este ejemplo se veria de la siguiente forma:

Anuncios
function productor()
	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 la funcion productor tendremos una variable que leera la informacion del archivo llamada x y luego la enviara por medio de enviar, en el caso de consumidor, tendremos una variable local llamada x donde por medio de la funcion recibir que es la encargada de recibir la informacion enviada por enviar y luego escribira la informacion en otro archivo, en esta implementacion ambas funciones corren por siempre, aunque es facil cambiarlos para detenerlos cuando no haya mas informacion para manejar, lo dificil de este caso es como lograr coincidir enviar y recibir, este es un tipico ejemplo de problema de quien tiene el bucle principal ya que ambos, productor y consumidor, estan activos, ambos tienen sus propios bucles principales y ambos asumen que el otro es un servicio llamable, para este particular ejemplo es facil cambiar la estructura de una de sus funciones transformando uno de los bucles y convirtiendolo en un agente pasivo, sin embargo este cambio de estructura quizas este lejos de una posible solucion en otros posibles escenarios.

Para este caso corutinas es un excelente herramienta para coincidir a productores y consumidores porque un par resume-yield convierte en arriba-abajo la tipica relacion entre llamador y llamado, cuando una corutina llama a yield esta no entra en una nueva funcion sino que regresa una llamada pendiente (resume), de forma similar una llamada a resume no comienza una nueva funcion pero regresa una llamada a yield, esta propiedad es exactamente lo que necesitamos para coincidir a enviar con un recibir de una manera tal que cada uno actua como si uno fuera el amo y otro el esclavo, asi que recibir reanuda al productor y de esta forma produce un nuevo valor y envia yield con el nuevo valor de vuelta a consumidor:

Anuncios
function recibir()
	local estado, valor = coroutine.resume(productor)
	return valor
end

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

Tambien deberemos modificar a la funcion productor de la siguiente forma:

productor = coroutine.create(
	function ()
		while true do
			local x = io.read()
			enviar(x)
		end
	end)

En este diseño, el programa comienza llamando a consumidor cuando este necesite un item reanuda al productor, el cual corre hasta que consigue un item para el consumidor y lo detiene hasta que el consumidor lo reanuda de nuevo, tenemos lo que llamamos un diseñado orientado al consumidor, a este diseño podemos extenderlo por medio de filtros, las cuales son tareas que se ubican entre el productor y el consumidor haciendo una especie de transformacion en los datos, un filtro es un productor y un consumidor al mismo tiempo asi que reanuda a un productor para conseguir nuevos valores y produce (yields) los valores transformados a un consumidor, como un ejemplo trivial podemos agregar a nuestro codigo previo un filtro que ingresa un numero de linea al comienzo de cada una, veamos el codigo:

function recibir(prod)
	local estado, valor = coroutine.resume(prod)
	return valor
end

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

function productor(x)
	return coroutine.create(function()
		while true do
			local x = io.read()
			enviar(x)
		end
	end)
end

function filtro(prod)
	return coroutine.create(function()
		for linea = 1, math.huge do
			local x = recibir(prod)
			x = string.format("%5d %s", linea, x)
			enviar(x)
		end
	end)
end

function consumidor(prod)
	while true do
		local x = recibir(prod)
		io.write(x, "\n")
	end
end

En este caso todas las funciones son como las vimos hasta ahora pero lo unico que agregamos fue una funcion llamada filtro la cual retorna una corutina donde por medio de un bucle for cambiara el formato de la informacion recibida en x por medio de recibir y una vez cambiado enviamos la informacion cambiada por medio de enviar, para poder utilizar este codigo lo haremos de la siguiente manera:

p = productor()
f = filtro(p)
consumidor(f)
Anuncios

O de mejor forma:

consumidor(filtro(productor()))

Veamos el siguiente ejemplo que es una funcion para generar los primeros n elementos de a:

function permgen (a,n)
	n = n or #a
	if n <= 1 then
		printResult(a)
	else
		for i = 1, n do
			a[n], a[i] = a[i], a[n]
			permgen(a, n-1)
			a[n], a[i] = a[i], a[n]
		end
	end
end

Si despues de leer el ejemplo anterior piensas en los pipes de Unix, buenas noticias no estas solo, despues de todo las corutinas son una especie de (no preventivo) multiproceso, mientras con pipes cada tarea corre por un proceso separado con corutinas cada tarea corre por una corutina separada, los pipes producen un buffer entre el escritor (productores) y los consumidores (consumidores) asi hay una cierta libertad en sus relativas velocidades, esto es importante en el contexto de pipes porque el costo del intercambio entre procesos es alto, pero con corutinas el costo de intercambio entre tareas es mucho mas chico y asi el escritor y el lector puede ir de la mano al mismo tiempo.

Anuncios

En resumen, hoy hemos pipes y filtros, como trabajan, como se usan, para que sirven, cuales son sus beneficios y como podemos sacar buen provecho de ellos, como recomendacion mas adelante hablaremos de esto y sera conveniente volver a repasar este y los posts relacionado a corutinas, espero les haya sido util sigueme en Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.

Tambien podes donar

Es para mantenimiento del sitio, gracias!

$1.00