Canales y GoRoutines en AjSharp (Part 2)

Published on Author lopezLeave a comment

En mi anterior post describí algo de la implementación de canales y “goroutines” en AjSharp, mi intérprete de un lenguaje de scripting. Quisiera hoy mostrar algunos ejemplos del uso de esos canales y rutinas lanzadas en paralelo.

Primero, recordemos el código simple:

channel = new Channel();
go channel <- 10;
result = <- channel;

En la primera línea, se crea el canal. Luego, el comando go ejecuta en otro thread el envío del valor 10 al canal. En la última línea se toma el valor desde el canal, y se coloca en la variable result. channel <- 10 es “syntax sugar” para channel.Send(10). <-channel es una expresión (devuelve un valor, no es un comando, se puede colocar en cualquier lugar donde se espera una expresión), que codifica channel.Receive(). Las operaciones Send y Receive son bloqueantes: cuando enviamos un valor a un canal, si no hay otro thread leyendo, el thread que envía queda bloqueado, y viceversa. Esta conducta nos da una forma de coordinación entre el productor de valores para el canal y el consumidor de esos valores. Tengo que mejorar el código de implementación para soportar que varios productores (en distintos threads) puedan enviar valores a un mismo canal. Lo mismo para varios consumidores.

Podemos usar el canal varias veces:

channel = new Channel();
go for (k=1; k<=5; k++) channel.Send(k);
for (j=1; j<=5; j++)
  result = result + channel.Receive();

que en notación de operadores, es lo mismo que:

channel = new Channel();
go for (k=1; k<=5; k++) channel <- k;
for (j=1; j<=5; j++)
  result = result + <-channel;

En vez de generar solamente cinco valores, podemos escribir una goroutine que escriba todos los números en un canal:

channel = new Channel();
running = true;
k = 0;
go while(running) channel <- k++;
for (value = <-channel; value<=10; value = <-channel)
  PrintLine(value);

La goruoutine no ejecuta por siempre: su ejecución es controlada, indirectamente, por el thread principal, usando las operaciones bloqueantes del canal. Si la goroutine intenta escribir en el canal, pero no hay quien esté leyendo, entonces la goroutine se bloquea.

Más interesante, podemos usar varios canales para comunicar distintos subprocesos:

channel = new Channel();
running = true;
k = 0;
go while(running) channel <- k++;
function filter(in, out)
{
  while (true) 
  {
    value = <-in;
    PrintLine("Received in filter " + value);
    if (value % 2)
      out <- value;
  }
}
odds = new Channel();
go filter(channel, odds);
for (number = <-odds; number <= 7; number = <-odds) 
  PrintLine("Received in main " + number);
  
running = false;

Este código crea un canal, y un thread paralelo que envía números naturales por ese canal. Otro thread toma valores del primer canal, filtra los impares (rechazando los pares) y los envía a un segundo canal. El thread principal toma valores de este segundo canal, e imprime los primeros resultados.

Vemos que podemos crear, conectar y usar muchos canales. El ejemplo que sigue (inspirado en el que brinda la gente de Google para el  Go language ver http://golang.org/doc/go_tutorial.html#tmp_346) imprime los números primos menores de 1000:

numbers = new Channel();
running = true;
k = 1;
go while(running) { k++; numbers <- k; }
function filter(in, out, prime)
{
  while (true) 
  {
    value = <-in;
    if (value % prime)
      out <- value;
  }
}
function makefilter(channel, number)
{
  newchannel = new Channel();
  go filter(channel, newchannel, number);
  return newchannel;
}
channel = numbers;
number = <-channel;
while (number < 1000) 
{
  PrintLine("Prime " + number);
  
  channel = makefilter(channel, number);
  
  number = <-channel;
}
running = false;

Esto planeando agregar un comando done, para parar las goroutines que quedaron andando (una idea más liviana es que las goroutines se lanzen en threads de background). Por ahora, no necesité esa característica. Tengo que estudar el lenguaje Go más en detalle: al parecer, las goroutines se ejecutan en un solo thread, pero no estoy seguro. Para leer más:

http://scienceblogs.com/goodmath/2009/11/the_go_i_forgot_concurrency_an.php

Sutil tema: en el código de arriba, escribí una función makefilter. En mi primer intento, escribí ese código en línea, no como función, directamente dentro del ciclo while, pero tuve un problema: la rutina lanzada por go tiene acceso a las variables que estan en su “lexical scope”, y entonces, cuando la go ruitina accede, por ejemplo, a la variable channel, ese valor podría no ser el mismo que tenía cuando se pasó por el go (la rutina de dentro del go se ejecuta en paralelo), el código externo podría haberlo cambiado. La solución: escribir una función makefilter (que no es otra cosa que un lambda con nombre) para recibir y mantener el valor original de channel y otros valores, sin verse afectados por la rutina principal. En el ejemplo del lenguaje Go que cité arriba, el autor escribe la manipulación del canal en línea, sin apelar a una rutina (por lo menos en el primer ejemplo que muestra), así que debería estudiar más en detalle este tema.

Pueden bajar el código actual de AjSharp desde http://code.google.com/p/ajcodekatas/source/browse/#svn/trunk/AjLanguage. Los ejemplos presentados acá están en AjSharp.Tests/Examples y en AjSharp.Console/Examples (AjSharp.Console es una aplicación de consola que permite ejecutar programas AjSharp leyendo archivos o ingresando el código interactivamente).

Me gusta cómo queda el código del ejemplo de números primos en pastie http://pastie.org/758175 😉

Mis próximos pasos en canales y gorutinas: implementarlos y usarlos directamente en C# (ayer escribí un spike), y agregar a un proyecto como el AjAgents o similar. Pueden ver el spike en:

http://code.google.com/p/ajcodekatas/source/browse/#svn/trunk/AjConcurr

y el código de ejemplo de números primos en C# directo:

http://pastie.org/761916

Nos leemos!

Angel “Java” Lopez

http://www.ajlopez.com

http://twitter.com/ajlopez

Leave a Reply

Your email address will not be published. Required fields are marked *