Una vez entendidos los enlaces y los monitores, queda otro problema por resolver. Utilicemos las siguientes funciones del módulo linkmon.erl:
start_critic() ->
spawn(?MODULE, critic, []).
judge(Pid, Band, Album) ->
Pid ! {self(), {Band, Album}},
receive
{Pid, Criticism} -> Criticism
after 2000 ->
timeout
end.
critic() ->
receive
{From, {"Rage Against the Turing Machine", "Unit Testify"}} ->
From ! {self(), "They are great!"};
{From, {"System of a Downtime", "Memoize"}} ->
From ! {self(), "They're not Johnny Crash but they're good."};
{From, {"Johnny Crash", "The Token Ring of Fire"}} ->
From ! {self(), "Simply incredible."};
{From, {_Band, _Album}} ->
From ! {self(), "They are terrible!"}
end,
critic().
Ahora vamos a fingir que vamos a las tiendas a comprar música. Hay algunos álbumes que suenan interesantes, pero nunca estamos del todo seguros. Decides llamar a tu amigo, el crítico.
1> c(linkmon).
{ok,linkmon}
2> Critic = linkmon:start_critic().
<0.47.0>
3> linkmon:judge(Critic, "Genesis", "The Lambda Lies Down on Broadway").
"They are terrible!"
Debido a una tormenta solar (estoy tratando de encontrar algo realista aquí), la conexión se interrumpe:
4> exit(Critic, solar_storm).
true
5> linkmon:judge(Critic, "Genesis", "A trick of the Tail Recursion").
timeout
Es molesto. Ya no podemos recibir críticas por los álbumes. Para mantener viva la crítica, escribiremos un proceso básico de "supervisión" cuya única función es reiniciarlo cuando deje de funcionar:
start_critic2() ->
spawn(?MODULE, restarter, []).
restarter() ->
process_flag(trap_exit, true),
Pid = spawn_link(?MODULE, critic, []),
receive
{'EXIT', Pid, normal} -> % not a crash
ok;
{'EXIT', Pid, shutdown} -> % manual termination, not a crash
ok;
{'EXIT', Pid, _} ->
restarter()
end.
Aquí, el reiniciador será su propio proceso. A su vez, iniciará el proceso del crítico y, si alguna vez muere por una causa anormal, restarter/0 se repetirá y creará un nuevo crítico. Tenga en cuenta que agregué una cláusula para {'EXIT', Pid, shutoff} como una forma de matar manualmente al crítico si alguna vez lo necesitamos.
El problema con nuestro enfoque es que no hay forma de encontrar el Pid del crítico y, por lo tanto, no podemos llamarlo para conocer su opinión. Una de las soluciones que Erlang tiene para resolver esto es dar nombres a los procesos.
El acto de dar un nombre a un proceso le permite reemplazar el pid impredecible por un átomo. Este átomo puede usarse exactamente como un Pid al enviar mensajes. Para darle un nombre a un proceso, se usa la función erlang:register/2. Si el proceso muere, perderá automáticamente su nombre o también puede usar unregister/1 para hacerlo manualmente. Puede obtener una lista de todos los procesos registrados con register/0 o una lista más detallada con el comando de shell regs(). Aquí podemos reescribir la función restarter/0 de la siguiente manera:
restarter() ->
process_flag(trap_exit, true),
Pid = spawn_link(?MODULE, critic, []),
register(critic, Pid),
receive
{'EXIT', Pid, normal} -> % not a crash
ok;
{'EXIT', Pid, shutdown} -> % manual termination, not a crash
ok;
{'EXIT', Pid, _} ->
restarter()
end.
Como puede ver, register/2 siempre le dará a nuestro crítico el nombre 'critic', sin importar cuál sea el Pid. Lo que debemos hacer es eliminar la necesidad de pasar un Pid desde las funciones de abstracción. Probemos esto:
judge2(Band, Album) ->
critic ! {self(), {Band, Album}},
Pid = whereis(critic),
receive
{Pid, Criticism} -> Criticism
after 2000 ->
timeout
end.
Aquí, la línea Pid = whereis(critic) se utiliza para encontrar el identificador de proceso del crítico con el fin de realizar una comparación de patrones con él en la expresión de recepción. Queremos hacer una comparación con este pid, porque nos asegura que encontraremos el mensaje correcto (¡podría haber 500 de ellos en el buzón mientras hablamos!). Sin embargo, esto puede ser la fuente de un problema. El código anterior supone que el pid del crítico seguirá siendo el mismo entre las dos primeras líneas de la función. Sin embargo, es completamente plausible que suceda lo siguiente:
1. critic ! Message
2. critic receives
3. critic replies
4. critic dies
5. whereis fails
6. critic is restarted
7. code crashes
O bien, también es una posibilidad:
1. critic ! Message
2. critic receives
3. critic replies
4. critic dies
5. critic is restarted
6. whereis picks up
wrong pid
7. message never matches
La posibilidad de que las cosas salgan mal en un proceso diferente puede hacer que salga mal otro si no hacemos las cosas bien. En este caso, el valor del átomo crítico se puede ver desde varios procesos. Esto se conoce como estado compartido. El problema aquí es que el valor del átomo crítico puede ser accedido y modificado por diferentes procesos prácticamente al mismo tiempo, lo que da como resultado información inconsistente y errores de software. El término común para este tipo de cosas es condición de carrera. Las condiciones de carrera son particularmente peligrosas porque dependen del momento en que ocurren los eventos. En casi todos los lenguajes concurrentes y paralelos que existen, este momento depende de factores impredecibles, como qué tan ocupado está el procesador, a dónde van los procesos y qué datos está procesando su programa.
Es posible que haya escuchado que Erlang generalmente no tiene condiciones de carrera ni interbloqueos y hace que el código paralelo sea seguro. Esto es cierto en muchas circunstancias, pero nunca suponga que su código es realmente tan seguro. Los procesos nombrados son solo un ejemplo de las múltiples formas en que el código paralelo puede salir mal.
Otros ejemplos incluyen el acceso a archivos en la computadora (para modificarlos), la actualización de los mismos registros de la base de datos desde muchos procesos diferentes, etc.
Afortunadamente para nosotros, es relativamente fácil corregir el código anterior si no asumimos que el proceso nombrado sigue siendo el mismo. En su lugar, utilizaremos referencias (creadas con make_ref()) como valores únicos para identificar mensajes. Necesitaremos reescribir la función critic/0 en critic2/0 y judge/3 en judge2/2:
judge2(Band, Album) ->
Ref = make_ref(),
critic ! {self(), Ref, {Band, Album}},
receive
{Ref, Criticism} -> Criticism
after 2000 ->
timeout
end.
critic2() ->
receive
{From, Ref, {"Rage Against the Turing Machine", "Unit Testify"}} ->
From ! {Ref, "They are great!"};
{From, Ref, {"System of a Downtime", "Memoize"}} ->
From ! {Ref, "They're not Johnny Crash but they're good."};
{From, Ref, {"Johnny Crash", "The Token Ring of Fire"}} ->
From ! {Ref, "Simply incredible."};
{From, Ref, {_Band, _Album}} ->
From ! {Ref, "They are terrible!"}
end,
critic2().
Y luego cambia restarter/0 para que se ajuste haciendo que genere critic2/0 en lugar de critic/0. Ahora las otras funciones deberían seguir funcionando bien. El usuario no notará ninguna diferencia. Bueno, la notará porque cambiamos el nombre de las funciones y cambiamos la cantidad de parámetros, pero no sabrá qué detalles de implementación se cambiaron y por qué fue importante. Todo lo que verá es que su código se simplificó y ya no necesita enviar un PID en las llamadas a funciones:
6> c(linkmon).
{ok,linkmon}
7> linkmon:start_critic2().
<0.55.0>
8> linkmon:judge2("The Doors", "Light my Firewall").
"They are terrible!"
9> exit(whereis(critic), kill).
true
10> linkmon:judge2("Rage Against the Turing Machine", "Unit Testify").
"They are great!"
Y ahora, aunque eliminamos al crítico, uno nuevo volvió instantáneamente para resolver nuestros problemas. Esa es la utilidad de los procesos nombrados. Si hubiera intentado llamar a linkmon:judge/2 sin un proceso registrado, el operador ! dentro de la función habría arrojado un error de argumento incorrecto, lo que garantizaría que los procesos que dependen de los nombrados no puedan ejecutarse sin ellos.
Los átomos se pueden usar en una cantidad limitada (aunque alta). Nunca debería crear átomos dinámicos. Esto significa que los procesos nombrados deben reservarse para servicios importantes exclusivos de una instancia de la máquina virtual y procesos que deberían estar allí durante todo el tiempo que se ejecuta su aplicación.
Si necesita procesos nombrados pero son transitorios o no hay ninguno que pueda ser exclusivo de la máquina virtual, puede significar que deben representarse como un grupo. Vincularlos y reiniciarlos juntos si fallan puede ser la opción más sensata, en lugar de intentar usar nombres dinámicos.