Probemos algo con la ayuda del comando pid(A,B,C), que nos permite convertir los 3 números enteros A, B y C en un pid. Aquí le daremos deliberadamente a kitchen:take/2 uno falso:
20> kitchen:take(pid(0,250,0), dog).
Ups. El shell está congelado. Esto sucedió debido a la forma en que se implementó take/2. Para entender lo que sucede, primero revisemos lo que sucede en el caso normal:
- Un mensaje para tomar comida se envía desde el shell al proceso del refrigerador;
- Su proceso cambia al modo de recepción y espera un nuevo mensaje;
- El refrigerador retira el artículo y lo envía a su proceso;
- Su proceso lo recibe y continúa con su vida.
Y esto es lo que sucede cuando el shell se congela:
- Un mensaje para tomar comida se envía desde el shell a un proceso desconocido;
- Su proceso cambia al modo de recepción y espera un nuevo mensaje;
- El proceso desconocido no existe o no espera tal mensaje y no hace nada con él;
- Su proceso de shell está bloqueado en modo de recepción.
Eso es molesto, especialmente porque no hay manejo de errores posible aquí. No sucedió nada ilegal, el programa solo está esperando. En general, cualquier cosa que tenga que ver con operaciones asincrónicas (que es como se hace el paso de mensajes en Erlang) necesita una forma de abandonar el proceso después de un cierto período de tiempo si no obtiene señales de recibir datos. Un navegador web lo hace cuando una página o imagen tarda demasiado en cargarse, usted lo hace cuando alguien tarda demasiado en responder el teléfono o llega tarde a una reunión. Erlang ciertamente tiene un mecanismo apropiado para eso, y es parte de la construcción de recepción:
receive
Match -> Expression1
after Delay ->
Expression2
end.
La parte entre recibir y después es exactamente la misma que ya conocemos. La parte después se activará si se ha pasado tanto tiempo como Delay (un entero que representa milisegundos) sin recibir un mensaje que coincida con el patrón Match. Cuando esto sucede, se ejecuta Expression2.
Escribiremos dos nuevas funciones de interfaz, store2/2 y take2/2, que actuarán exactamente como store/2 y take/2 con la excepción de que dejarán de esperar después de 3 segundos:
store2(Pid, Food) ->
Pid ! {self(), {store, Food}},
receive
{Pid, Msg} -> Msg
after 3000 ->
timeout
end.
take2(Pid, Food) ->
Pid ! {self(), {take, Food}},
receive
{Pid, Msg} -> Msg
after 3000 ->
timeout
end.
Ahora puedes descongelar el shell con ^G y probar las nuevas funciones de la interfaz:
User switch command
--> k
--> s
--> c
Eshell V5.7.5 (abort with ^G)
1> c(kitchen).
{ok,kitchen}
2> kitchen:take2(pid(0,250,0), dog).
timeout
Y ahora funciona.
After no solo toma milisegundos como valor, en realidad es posible usar el átomo infinito. Si bien esto no es útil en muchos casos (podría simplemente eliminar la cláusula after por completo), a veces se usa cuando el programador puede enviar el tiempo de espera a una función donde se espera recibir un resultado. De esa manera, si el programador realmente quiere esperar eternamente, puede hacerlo.
Existen usos para estos temporizadores además de darse por vencidos después de demasiado tiempo. Un ejemplo muy simple es cómo funciona la función timer:sleep/1 que hemos usado antes. Aquí se muestra cómo se implementa (pongámosla en un nuevo módulo multiproc.erl):
sleep(T) ->
receive
after T -> ok
end.
En este caso específico, nunca se encontrará ningún mensaje en la parte de recepción de la construcción porque no hay ningún patrón. En cambio, se llamará a la parte posterior de la construcción una vez que haya transcurrido el retraso T.
Otro caso especial es cuando el tiempo de espera es 0:
flush() ->
receive
_ -> flush()
after 0 ->
ok
end.
Cuando esto sucede, la máquina virtual Erlang intentará encontrar un mensaje que se ajuste a uno de los patrones disponibles. En el caso anterior, todo coincide. Mientras haya mensajes, la función flush/0 se llamará a sí misma de forma recursiva hasta que el buzón esté vacío. Una vez hecho esto, se ejecuta la parte after 0 -> ok del código y la función retorna.