Programación en Erlang/Implementación de corutinas
Procesos
editarErlang, a diferencia de otros lenguajes funcionales, tiene la capacidad de manejar concurrencia y programación distribuida. Concurrencia es poder manejar varios hilos de ejecución al mismo tiempo. Por ejemplo, sistemas operativos modernos le permitirían utilizar un procesador de textos, una hoja de cálculo, un cliente de correo y un trabajo de impresión todo esto ejecutándose al mismo tiempo. Por supuesto cada procesador (CPU) en el sistema probablemente sólo maneja un hilo (o trabajo) a la vez, pero cambia entre los trabajos a una velocidad que da la ilusión de correrlos todo al mismo tiempo. Es fácil crear hilos paralelos de ejecución en Erlang y es fácil permitir que estos hilos se comuniquen unos con otros. En Erlang a cada hilo de ejecución se le denomina proceso. Nota: El término "proceso" es utilizado, generalmente, cuando los hilos de ejecución no comparten datos uno con el otro y el termino "hilo" cuando ellos comparten datos de alguna manera. Los hilos de ejecución en Erlang no comparten datos, por eso se llaman procesos. El BIF (Built-in Function) spawn de Erlang es utilizado para crear un nuevo proceso spawn(Modulo, Funcion_Exportada, Lista de Argumentos) Ejemplo: (Parte I) -module(tut14).
-export([start/0, say_something/2]).
say_something(What, 0) ->
done;
say_something(What, Times) ->
io:format("~p~n", [What]), say_something(What, Times - 1).
start() ->
spawn(tut14, say_something, [hello, 3]), spawn(tut14, say_something, [goodbye, 3]).
5> c(tut14).
{ok,tut14}
6> tut14:say_something(hello, 3).
hello
hello
hello
done
Podemos observar que la función say_something escribe su primer argumento la cantidad de veces especificada por el segundo argumento. Ahora si observamos a la función start. Este inicia dos procesos, uno que escribe “hello” tres veces y otro que escribe “goodbye” tres veces. Ambos de los procesos utilizan la función say_something.
Nota: La función utilizada por spawn para iniciar un proceso fue exportado desde un modulo (Ej. En el -export al inicio del modulo).
(Parte II)
9> tut14:start().
hello
goodbye
<0.63.0>
hello
goodbye
hello
goodbye
Podemos notar que en esta parte no se escribió “hello” tres veces y después “goodbye” tres veces, en vez el primer proceso escribió “hello” y el Segundo “goodbye”, el siguiente otro “hello”, etc… Ahora, de donde sale el <0.63.0>? Lo que retorna una función es el valor de la ultima “cosa” que realize la función. Lo último que hace la función start es
spawn(tut14, say_something, [goodbye, 3]). spawn retorna el identificador de un proceso (Process Identifier o pid), este identifica de manera unica un proceso.Asi que <0.63.0> es el pid que la función spawn llama. Nota: Observe que en io:format se utilize ~p en vez de ~w. Esto es así ya que, ~p escribe la data en una sintaxis estándar, de la misma manera que ~w, sin embargo este rompe los terms que al imprimir son más largos de una línea en varias líneas y las indenta de manera coherente. También intenta detectar listas con caracteres imprimibles e imprimirlos como strings.
Como enviar mensajes
editarEn el siguiente ejemplo se crean dos procesos que se envían mensajes el uno al otro un determinado número de veces. Ejemplo: -module(tut15).
-export([start/0, ping/2, pong/0]).
ping(0, Pong_PID) ->
Pong_PID ! finished, io:format("ping finished~n", []);
ping(N, Pong_PID) ->
Pong_PID ! {ping, self()}, receive pong -> io:format("Ping received pong~n", []) end, ping(N - 1, Pong_PID).
pong() ->
receive finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong() end.
start() ->
Pong_PID = spawn(tut15, pong, []), spawn(tut15, ping, [3, Pong_PID]).
1> c(tut15). {ok,tut15} 2> tut15: start(). <0.36.0> Pong received ping Ping received pong Pong received ping Ping received pong Pong received ping Ping received pong ping finished Pong finished
La función start primero crea un proceso llamado “pong”:
Pong_PID = spawn(tut15, pong, [])
Este proceso ejecuta tut15:pong(). Pong_PID es la identidad del proceso “pong”. La función start ahora, crea un nuevo proceso llamado “ping”.
spawn(tut15, ping, [3, Pong_PID]),
este proceso ejecuta
tut15:ping(3, Pong_PID)
<0.36.0> es el valor de retorno de la función start.
El proceso “pong” ahora realiza lo siguiente:
receive
finished -> io:format("Pong finished~n", []); {ping, Ping_PID} -> io:format("Pong received ping~n", []), Ping_PID ! pong, pong()
end.
El contructor receive es usado para permitir que los procesos esperen mensajes de otros procesos. Tiene este formato:
receive
pattern1 -> actions1; pattern2 -> actions2; .... patternN actionsN
end.
Nota: antes del end no se utiliza un “;”. Mensajes entre procesos son validos únicamente entre los tipos de datos establecidos por Erlang. Pueden ser lists, tuples, integers, atoms, pids etc. Cada proceso tiene su propia cola para los mensajes que recibe. Los nuevos mensajes que recibe, son puestos al final de la cola. Cuando un proceso ejecuta receive, el primer mensaje en la cola es emparejado con el primer patrón en receive, si esto concuerda, el mensaje es removido de la cola y las acciones correspondientes al patrón son ejecutadas. Sin embargo, si el primer patrón no concuerda, el segundo patrón es probado, si este concuerda, el mensaje es removido de la cola y las acciones correspondientes al segundo patrón son ejecutadas. Si el segundo patrón no concuerda, el tercero es probado, etc. Así será hasta que ya no haya más patrones por probar. Si ya no existen más patrones por probar, el primer mensajes es retenido en la cola y se intenta con el segundo mensaje. Si este concuerda cualquier patrón, las acciones apropiadas son ejecutadas y el segundo mensaje es removido de la cola (manteniendo el primer y el resto de los mensajes en la cola). Si el segundo mensaje no concuerda se prueba el tercer mensaje, etc. Asi será hasta que se llegue al final de la cola. Si se llega al final de la cola, los procesos detienen su ejecucion y esperan hasta que un Nuevo mensaje sea recibido, este procedimiento de repite. La implementación en Erlang es “ingeniosa” y minimize el número de veces que cada mensaje es probado contra los patrones en receive. Continuando con el ejemplo de Ping-Pong: "Pong" esta esperando mensajes. Si el atom finished es recibido, “pong” imprime “pong finished” y como no tiene nada mas que hacer, finalize. Si recibe un mensaje en este formato: {ping, Ping_PID} Imprime "Pong received ping" y envia el atom pong al proceso "ping": Ping_PID ! pong
Nota: El operador “!” es utilizado para enviar mensajes. Su sintaxis es la siguiente: Pid ! Message En este caso Message (cualquier termino en Erlang) es enviado al proceso con identidad Pid. Después de enviar el mensaje pong, al proceso “ping”, “pong” llama a la función pong de nuevo, lo que causa regresar a receive otra vez y esperar otro mensaje. Ahora el proceso “ping” fue iniciado ejecutando: tut15:ping(3, Pong_PID) Viendo la función ping/2, observamos que la segunda clausula de ping/2 es ejecutada ya que el valor del primer argumento es 3 (no 0)(la primera clausula es ping(0,Pong_PID), segunda clausula es ping(N,Pong_PID), por consiguiente N se convierte en 3). La segunda clausula envía un mensaje a “pong”: Pong_PID ! {ping, self()}, self() retorna el pid del proceso y esto ejecuta self(), en este caso el pid de “ping”. (En el código de “pong” esto caerá en la variable Ping_PID en receive). "Ping" ahora deberá esperar por la respuesta de “pong”: receive
pong -> io:format("Ping received pong~n", [])
end, y escribe "Ping received pong" cuando esta respuesta haya llegado, después de esto “ping” llama a la función ping una vez más. ping(N - 1, Pong_PID) N-1 causa que el primer argumento sea disminuido hasta llegar a 0. Cuando esto ocurre, la primera clausula de ping/2 será ejecutada: ping(0, Pong_PID) ->
Pong_PID ! finished, io:format("ping finished~n", []);
El atom finished es enviado a "pong" (causando que el proceso termine) y se imprime "ping finished" en la ventana. "Ping" al no tener nada que hacer se finaliza.