aprendiendo ( Erlang ).

martes, 9 de agosto de 2011

Uso de los timeouts (Receive ... after ...)

| 2 comentarios |

A veces la instrucción receive podría esperar para siempre un mensaje. Ello, puede ocurrir por múltiples motivos: el proceso con el que nos comunicamos no esta arrancado; ha fallado la comunicación; ha cambiado el protocolo de mensajes y no hace matching; etc... Todos estos problemas podemos solucionarlos si añadimos un control de timeouts en nuestra instrucción receive. Es decir, indicamos a nuestra instrucción el tiempo máximo que el procesos debe esperar para recibir un mensaje. La sintaxis del receive con timeout es:
receive
  Patron1 [when Guarda1] ->
     secuencia de comandos1;
  ...
  PatronN [when GuardaN] ->
     secuencia de comandosN
after 
  Milisegundos ->
     secuencia de comandosTimeouts
end
Si salta el timeout en una instrucción receive quiere decir que, no se han recibidos mensajes que coincidan con nuestros patrones en el tiempo dado. En tal caso, se ejecutará la secuencia de comandosTimeouts.

La explicación anterior es un poco simplista. Taparse los oídos aquellos que no quieran oír palabrotas. Para explicar bien los timeouts hay que introducir el concepto de cola de mensajes. Todo proceso Erlang tiene una cola de mensajes y en ella se encolan los mensajes que se reciben. Una vez encolado el mensaje el sistema lo pasa a la instrucción receive que, si hace macthing lo procesa y en caso contrario lo conservará en la cola. El sistema puede desechar el mensaje pasado un tiempo. En definitiva, el timeout se produce cuando la cola de mensajes entrantes ha sido comprobada y a pasado el tiempo dado sin recibir nuevos mensajes.

Una vez realizada esta pequeña explicación entramos en faena. Para empezar, me he creado un modulo para los distintos ejemplos llamado timeouts.erl. Y en dicho módulo, he implementado dos funciones nada ostentosas, que me permitirá enviar mensajes de forma cómoda a cualquiera de mis ejemplos:
hola(PID) ->
    PID!hola.

terminar(PID) ->
    PID!terminar.
Creo que no merecen explicación por eso pasemos a ver nuestro primer ejemplo:
start_ejemplo1() -> 
    spawn(fun ejemplo1/0).

ejemplo1() ->
    receive
        terminar ->
            io:format("Finalizar~n");
        Mensaje ->
            io:format("Mensaje recibido ~p~n",[Mensaje]),
            ejemplo1()
    after 10000 ->
            io:format("Timeout 10 Segundos.~n")
    end.
Como podéis observar le he añadido un timeout de 10 segundos (10000 milisegundos). Un ejemplo sencillo y fácil de entender. Veamos como se comporta:
1> c(timeouts).
{ok,timeouts}
2> Pid=timeouts:start_ejemplo1(). 
<0.42.0>
3> timeouts:hola(Pid).
Mensaje recibido hola
hola
Timeout 10 Segundos.
4> timeouts:hola(Pid).
hola
Como podemos observar hasta que no salta el timeout el proceso esta recibiendo mensajes. Una vez que pasan los 10 segundos salta el timeout y proceso deja de recibir mensajes. Pero ... ¿Por qué? El funcionamiento normal es este, una vez ejecutado el timeout para el proceso a no ser, que le indiquemos lo contrario. He puesto este ejemplo porque, tengo un primo (es decir, yo), que hizo algo parecido y se volvía loco. Veamos como realizar un ejemplo que no interrumpa el proceso:
start_ejemplo2() -> 
    spawn(fun ejemplo2/0).

ejemplo2() ->
    receive
        terminar ->
            io:format("Finalizar~n");
        Mensaje ->
            io:format("Mensaje recibido ~p~n",[Mensaje]),
            ejemplo2()
    after 5000 ->
            io:format("Esperando mensajes.~n"),
            ejemplo2()
    end.
Y su comportamiento:
5> f().               
ok
6> Pid=timeouts:start_ejemplo2().
<0.47.0>
Esperando mensajes.              
Esperando mensajes.   
7> timeouts:hola(Pid).
Mensaje recibido hola
hola     
Esperando mensajes.
Esperando mensajes.
8> timeouts:terminar(Pid).
Finalizar
terminar 
Ahora sí, ahora podemos ver como el hecho de que se produzca el timeout no supone que el proceso se interrumpa.

Dependiendo de las necesidades utilizaremos un comportamiento u otro. Su pon un servidor y sólo deseamos saber cuando esta ocioso pues, el segundo ejemplo sería una aproximación, pero si lo que deseamos es realizar una conexión a un servicio, lo mejor es utilizar el primer ejemplo para saber si responde.

Sleep un ejemplo típico y/o tópico

Un ejemplo típico, o por lo menos lo he visto por todos lados, es la realización de una función sleep/1. Se trata de un función que interrumpe o para un proceso durante un tiempo.
sleep(Milisegundos) ->
    receive
    after Milisegundos ->
            true
    end.
Como puedes ver, el sleep/1 se consigue poniendo una instrucción receive que no procesa mensajes con un timeout parametrizado.

¿Timeout a cero?

Puede parecer inútil poner un timeout a cero. Pero en realidad no lo es. Pero ... ¿el timeout se ejecutaría inmediatamente? no, no lo haría. En esta explicación vuelve a entrar en juego el concepto de cola de mensajes. Como ya explique anteriormente, el timeout no sucede hasta que la cola ha sido procesada o este vacía. Esta propiedad se utiliza para vaciar la cola de mensajes entrantes.
limpiar_mensajes_entrantes() ->
  receive
    _Any ->
      limpiar_mensajes_entrantes()
  after 0 ->
      true
  end.

Prioridad de recepción

Otra utilidad del timeout es la de poder establecer prioridades en la recepción de mensajes.
start_ejemplo3(Retardo) -> 
    Pid = spawn(fun prioridad_mensajes/0),
    F = fun (X) ->
                Pid!X
        end,
    lists:foreach (F, lists:seq(1,10)),
    sleep(Retardo),
    Pid!terminar.


prioridad_mensajes() ->
    receive
        terminar ->
            io:format("Mensaje prioritario ~p ~n", [terminar])
    after 10 ->
            receive
                Otros ->
                    io:format("Mensaje no prioritario ~p ~n", [Otros]),
                    prioridad_mensajes()
            end
    end.
Empecemos explicando la función prioridad_mensajes/0. En ella, los mensajes prioritarios son los mensajes terminar, en caso de no existir ningún mensaje terminar saltaría su timeout y pasaría a procesar otros mensajes.

Para ilustrar el uso de prioridades con los timeouts la función start_ejemplo3/1 envía diez mensajes y un terminar al proceso después de un retardo. Veamos la ejecución:
9> timeouts:start_ejemplo3(0). 
Mensaje prioritario terminar 
terminar
10> timeouts:start_ejemplo3(10).
Mensaje no prioritario 1 
Mensaje prioritario terminar 
terminar
11> timeouts:start_ejemplo3(20).
Mensaje no prioritario 1 
Mensaje prioritario terminar 
terminar
12> timeouts:start_ejemplo3(30).
Mensaje no prioritario 1 
Mensaje no prioritario 2 
Mensaje prioritario terminar 
terminar
13> timeouts:start_ejemplo3(40).
Mensaje no prioritario 1 
Mensaje no prioritario 2 
Mensaje no prioritario 3 
Mensaje prioritario terminar 
terminar
14> timeouts:start_ejemplo3(100).
Mensaje no prioritario 1 
Mensaje no prioritario 2 
Mensaje no prioritario 3 
Mensaje no prioritario 4 
Mensaje no prioritario 5 
Mensaje no prioritario 6 
Mensaje no prioritario 7 
Mensaje no prioritario 8 
Mensaje no prioritario 9 
Mensaje prioritario terminar 
terminar
15> timeouts:start_ejemplo3(110).
Mensaje no prioritario 1 
Mensaje no prioritario 2 
Mensaje no prioritario 3 
Mensaje no prioritario 4 
Mensaje no prioritario 5 
Mensaje no prioritario 6 
Mensaje no prioritario 7 
Mensaje no prioritario 8 
Mensaje no prioritario 9 
Mensaje no prioritario 10 
Mensaje prioritario terminar 
terminar
Como podemos observar dependiendo del retardo quedemos al mensaje prioritario el proceso consumirá más o menos mensajes no prioritarios antes de terminar.
Aclaraciones al ejemplo de prioridades:
  1. He puesto un timeout de 10 milisegundos para que el ejemplo sea lo suficientemente ilustrativo y aclaratorio pero lo ideal sería un timeout a cero, ya que estamos provocando un retardo de 10 milisegundos como intervalo de recepción de mensajes.
  2. ¿Qué pasa si recibimos un mensaje terminar después del timeout? La respuesta a esto es clara nunca terminaría el proceso a no ser que recibamos un mensaje no prioritario. Lo apropiado sería que la recepción de o los mensajes prioritarios se realicen también en las instrucciones receive internas, para evitar este problema.
  3. En colas de mensajes largas es extremadamente ineficiente por que cada vez que se trata un mensaje hay que comprobar la existencia de mensajes prioritarios. Y claro esta, cuanto más grande sea el volumen de mensajes más lento es este proceso.

Timeout a infinito

Existe también un átomo infinity que es aceptado como timeout. Al que no le encuentro mucho sentido ya que las sentencias del timeout nunca se ejecutaran.

He leído que se utiliza para procesos en el cual, el cálculo del timeout queda fuera de la instrucción receive o que son calculados en tiempo de ejecución.

Si tenéis algún ejemplo de uso de infinity o sabéis como funciona estaré encantado de ampliar este apartado.

Publicar un comentario

2 comentarios:

  1. Jesús Hernández Gormaz dijo...

    Respecto a lo del timeout infinity, usarlo directamente como atomo tampoco yo le veo sentido pero si pasado como parametro, por ejemplo:
    {ok, Channel_ssh} = ssh_connection:session_channel(Connection_ssh, infinity).
    En cualquier biblioteca a la cual no pueda o no quiera modificar el código para que no tenga un timeout se lo puedo quitar pasandole infinity como parametro de tiempo de espera.

  2. Verdi dijo...

    Hola Jesús:

    Amén a ello. Gracias por la aclaración

 
Licencia Creative Commons
Aprendiendo Erlang por Verdi se encuentra bajo una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0 Unported.