El concepto de 'flushing' permite implementar una recepción selectiva que puede dar prioridad a los mensajes que recibe mediante la anidación de llamadas:
important() ->
receive
{Priority, Message} when Priority > 10 ->
[Message | important()]
after 0 ->
normal()
end.
normal() ->
receive
{_, Message} ->
[Message | normal()]
after 0 ->
[]
end.
Esta función creará una lista de todos los mensajes, comenzando primero con aquellos con una prioridad superior a 10:
1> c(multiproc).
{ok,multiproc}
2> self() ! {15, high}, self() ! {7, low}, self() ! {1, low}, self() ! {17, high}.
{17,high}
3> multiproc:important().
[high,high,low,low]
Como utilicé el bit posterior a 0, se obtendrán todos los mensajes hasta que no quede ninguno, pero el proceso intentará capturar todos aquellos con una prioridad superior a 10 antes incluso de considerar los demás mensajes, que se acumulan en la llamada normal/0.
Si esta práctica le parece interesante, tenga en cuenta que a veces no es segura debido a la forma en que funcionan las recepciones selectivas en Erlang.
Cuando se envían mensajes a un proceso, se almacenan en el buzón hasta que el proceso los lee y coinciden con un patrón allí. Los mensajes se almacenan en el orden en que se recibieron. Esto significa que cada vez que coincide con un mensaje, comienza por el más antiguo.
Luego, ese mensaje más antiguo se prueba con cada patrón de la recepción hasta que uno de ellos coincide. Cuando lo hace, el mensaje se elimina del buzón y el código del proceso se ejecuta normalmente hasta la próxima recepción. Cuando se evalúa esta próxima recepción, la máquina virtual buscará el mensaje más antiguo que se encuentre actualmente en el buzón (el siguiente al que eliminamos), y así sucesivamente.
Cuando no hay forma de encontrar una coincidencia con un mensaje determinado, se lo coloca en una cola de guardado y se intenta con el siguiente mensaje. Si el segundo mensaje coincide, el primero se vuelve a colocar en la parte superior del buzón para volver a intentarlo más tarde.
Esto le permite preocuparse únicamente por los mensajes que son útiles. Ignorar algunos mensajes para manejarlos más tarde de la manera descrita anteriormente es la esencia de las recepciones selectivas. Si bien son útiles, el problema con ellas es que si su proceso tiene muchos mensajes que nunca le interesan, leer los mensajes útiles en realidad llevará cada vez más tiempo (y los procesos también crecerán en tamaño).
Imagine que queremos el mensaje n.° 367, pero los primeros 366 son basura ignorada por nuestro código. Para obtener el mensaje n.° 367, el proceso debe intentar hacer coincidir los 366 primeros. Una vez que haya terminado y todos se hayan colocado en la cola, se saca el mensaje n.° 367 y los primeros 366 se vuelven a colocar en la parte superior del buzón. El siguiente mensaje útil podría estar mucho más escondido y tardar aún más en encontrarlo.
Este tipo de recepción es una causa frecuente de problemas de rendimiento en Erlang. Si su aplicación se ejecuta con lentitud y sabe que hay muchos mensajes circulando, esta podría ser la causa.
Si estas recepciones selectivas están causando una ralentización masiva de su código, lo primero que debe hacer es preguntarse por qué recibe mensajes que no desea. ¿Se envían los mensajes a los procesos correctos? ¿Son correctos los patrones? ¿Los mensajes tienen un formato incorrecto? ¿Está utilizando un proceso cuando debería haber muchos? Responder a una o varias de estas preguntas podría resolver su problema.
Debido a los riesgos de que los mensajes inútiles contaminen el buzón de un proceso, los programadores de Erlang a veces toman una medida defensiva contra tales eventos. Una forma estándar de hacerlo podría ser la siguiente:
receive
Pattern1 -> Expression1;
Pattern2 -> Expression2;
Pattern3 -> Expression3;
...
PatternN -> ExpressionN;
Unexpected ->
io:format("unexpected message ~p~n", [Unexpected])
end.
Lo que esto hace es asegurarse de que cualquier mensaje coincida con al menos una cláusula. La variable Unexpected coincidirá con cualquier cosa, sacará el mensaje inesperado del buzón y mostrará una advertencia. Dependiendo de su aplicación, es posible que desee almacenar el mensaje en algún tipo de servicio de registro donde podrá encontrar información sobre él más adelante: si los mensajes van al proceso equivocado, sería una pena perderlos para siempre y tener dificultades para encontrar por qué ese otro proceso no recibe lo que debería.
En el caso de que necesite trabajar con una prioridad en sus mensajes y no pueda usar una cláusula de captura general, una forma más inteligente de hacerlo sería implementar un min-heap o usar el módulo gb_trees y volcar todos los mensajes recibidos en él (asegúrese de poner el número de prioridad primero en la clave para que se use para ordenar los mensajes). Luego, puede simplemente buscar el elemento más pequeño o más grande en la estructura de datos según sus necesidades.
En la mayoría de los casos, esta técnica debería permitirle recibir mensajes con una prioridad de manera más eficiente que las recepciones selectivas. Sin embargo, podría ralentizarlo si la mayoría de los mensajes que recibe tienen la máxima prioridad posible. Como siempre, el truco es perfilar y medir antes de optimizar.
Desde R14A, se agregó una nueva optimización al compilador de Erlang. Simplifica las recepciones selectivas en casos muy específicos de comunicaciones de ida y vuelta entre procesos. Un ejemplo de una función de este tipo es optimized/1 en multiproc.erl.
-module(multiproc).
-compile([export_all]).
sleep(T) ->
receive
after T -> ok
end.
flush() ->
receive
_ -> flush()
after 0 ->
ok
end.
important() ->
receive
{Priority, Message} when Priority > 10 ->
[Message | important()]
after 0 ->
normal()
end.
normal() ->
receive
{_, Message} ->
[Message | normal()]
after 0 ->
[]
end.
%% optimized in R14A
optimized(Pid) ->
Ref = make_ref(),
Pid ! {self(), Ref, hello},
receive
{Pid, Ref, Msg} ->
io:format("~p~n", [Msg])
end.
Para que funcione, se debe crear una referencia (make_ref()) en una función y luego enviarla en un mensaje. En la misma función, se realiza una recepción selectiva. Si ningún mensaje puede coincidir a menos que contenga la misma referencia, el compilador se asegura automáticamente de que la máquina virtual omitirá los mensajes recibidos antes de la creación de esa referencia.
Tenga en cuenta que no debe intentar forzar su código para que se ajuste a dichas optimizaciones. Los desarrolladores de Erlang solo buscan patrones que se usan con frecuencia y luego los hacen más rápidos. Si escribe código idiomático, las optimizaciones deberían venir a usted. No al revés.
Con estos conceptos entendidos, el siguiente paso será realizar el manejo de errores con múltiples procesos.