Vamos a ver las tres primitivas necesarias para la concurrencia en Erlang: generar nuevos procesos, enviar mensajes y recibir mensajes. En la práctica, se requieren más mecanismos para crear aplicaciones realmente confiables, pero por ahora esto será suficiente.
Me he saltado mucho el tema y todavía tengo que explicar qué es realmente un proceso. De hecho, no es más que una función. Eso es todo. Ejecuta una función y, una vez que termina, desaparece. Técnicamente, un proceso también tiene algún estado oculto (como un buzón para mensajes), pero las funciones son suficientes por ahora.
Para iniciar un nuevo proceso, Erlang proporciona la función spawn/1, que toma una sola función y la ejecuta:
1> F = fun() -> 2 + 2 end.
#Fun<erl_eval.20.67289768>
2> spawn(F).
<0.44.0>
El resultado de spawn/1 (<0.44.0>) se denomina Identificador de proceso, que la comunidad suele escribir simplemente PID, Pid o pid. El identificador de proceso es un valor arbitrario que representa cualquier proceso que exista (o pueda haber existido) en algún momento de la vida de la máquina virtual. Se utiliza como una dirección para comunicarse con el proceso.
Notarás que no podemos ver el resultado de la función F. Solo obtenemos su pid. Esto se debe a que los procesos no devuelven nada. ¿Cómo podemos ver entonces el resultado de F? Bueno, hay dos formas. La más fácil es simplemente mostrar lo que obtenemos:
3> spawn(fun() -> io:format("~p~n",[2 + 2]) end).
4
<0.46.0>
Esto no es práctico para un programa real, pero es útil para ver cómo Erlang distribuye los procesos. Afortunadamente, usar io:format/2 es suficiente para permitirnos experimentar. Iniciaremos 10 procesos muy rápido y pausaremos cada uno de ellos por un tiempo con la ayuda de la función timer:sleep/1, que toma un valor entero N y espera N milisegundos antes de reanudar el código. Después del retraso, se muestra el valor presente en el proceso.
4> G = fun(X) -> timer:sleep(10), io:format("~p~n", [X]) end.
#Fun<erl_eval.6.13229925>
5> [spawn(fun() -> G(X) end) || X <- lists:seq(1,10)].
[<0.273.0>,<0.274.0>,<0.275.0>,<0.276.0>,<0.277.0>,
<0.278.0>,<0.279.0>,<0.280.0>,<0.281.0>,<0.282.0>]
2
1
4
3
5
8
7
6
10
9
El orden no tiene sentido. Bienvenido al paralelismo. Debido a que los procesos se ejecutan al mismo tiempo, el orden de los eventos ya no está garantizado. Esto se debe a que la máquina virtual Erlang utiliza muchos trucos para decidir cuándo ejecutar un proceso u otro, asegurándose de que cada uno tenga una buena parte del tiempo. Muchos servicios de Erlang se implementan como procesos, incluido el shell en el que está escribiendo. Sus procesos deben equilibrarse con los que necesita el propio sistema y esta podría ser la causa del orden extraño.
Los resultados son similares independientemente de si el multiprocesamiento simétrico está habilitado o no. Para comprobarlo, puede probarlo iniciando la máquina virtual Erlang con $ erl -smp disable
Para ver si su máquina virtual Erlang se ejecuta con o sin soporte SMP en primer lugar, inicie una nueva máquina virtual sin ninguna opción y busque la salida de la primera línea. Si puede ver el texto [smp:2:2] [rq:2], significa que está ejecutando con SMP habilitado y que tiene 2 colas de ejecución (rq o programadores) ejecutándose en dos núcleos. Si solo ve [rq:1], significa que está ejecutando con SMP deshabilitado.
Si desea saberlo, [smp:2:2] significa que hay dos núcleos disponibles, con dos programadores. [rq:2] significa que hay dos colas de ejecución activas. En versiones anteriores de Erlang, podía tener varios programadores, pero con solo una cola de ejecución compartida. Desde R13B, hay una cola de ejecución por programador de manera predeterminada; esto permite un mejor paralelismo.
Para demostrar que el shell en sí está implementado como un proceso regular, usaré el BIF self/0, que devuelve el pid del proceso actual:
6> self().
<0.41.0>
7> exit(self()).
** exception exit: <0.41.0>
8> self().
<0.285.0>
Y el pid cambia porque el proceso se ha reiniciado. Los detalles de cómo funciona esto se verán más adelante. Por ahora, hay cosas más básicas que cubrir. La más importante en este momento es averiguar cómo enviar mensajes, porque nadie quiere quedarse atascado con la salida de los valores resultantes de los procesos todo el tiempo y luego ingresarlos manualmente en otros procesos (al menos yo sé que no).
El siguiente primitivo necesario para realizar el paso de mensajes es el operador !, también conocido como el símbolo bang. En el lado izquierdo toma un pid y en el lado derecho toma cualquier término de Erlang. Luego, el término se envía al proceso representado por el pid, que puede acceder a él:
9> self() ! hello.
hello
El mensaje se ha colocado en el buzón del proceso, pero aún no se ha leído. El segundo saludo que se muestra aquí es el valor de retorno de la operación de envío. Esto significa que es posible enviar el mismo mensaje a muchos procesos haciendo lo siguiente:
10> self() ! self() ! double.
double
Lo cual es equivalente a self() ! (self() ! double). Una cosa a tener en cuenta sobre el buzón de un proceso es que los mensajes se guardan en el orden en que se reciben. Cada vez que se lee un mensaje, se saca del buzón.
Para ver el contenido del buzón actual, puede usar el comando flush() mientras está en el shell:
11> flush().
Shell got hello
Shell got double
Shell got double
ok
Esta función es simplemente un atajo que muestra los mensajes recibidos. Esto significa que todavía no podemos vincular el resultado de un proceso a una variable, pero al menos sabemos cómo enviarlo de un proceso a otro y verificar si se ha recibido.
Enviar mensajes que nadie leerá es tan útil como escribir poesía emo; no mucho. Por eso necesitamos la declaración de recepción. En lugar de jugar demasiado tiempo en el shell, escribiremos un programa corto sobre los delfines para aprender sobre ellos:
-module(dolphins).
-compile(export_all).
dolphin1() ->
receive
do_a_flip ->
io:format("How about no?~n");
fish ->
io:format("So long and thanks for all the fish!~n");
_ ->
io:format("Heh, we're smarter than you humans.~n")
end.
Como puede ver, la sintaxis de receive es similar a la de case ... of. De hecho, los patrones funcionan exactamente de la misma manera, excepto que vinculan variables que provienen de mensajes en lugar de la expresión entre caso y de. Los patrones de recepción también pueden tener guards:
receive
Pattern1 when Guard1 -> Expr1;
Pattern2 when Guard2 -> Expr2;
Pattern3 -> Expr3
end
Ahora podemos compilar el módulo anterior, ejecutarlo y comenzar a comunicarnos con los delfines:
11> c(dolphins).
{ok,dolphins}
12> Dolphin = spawn(dolphins, dolphin1, []).
<0.40.0>
13> Dolphin ! "oh, hello dolphin!".
Heh, we're smarter than you humans.
"oh, hello dolphin!"
14> Dolphin ! fish.
fish
15>
Aquí presentamos una nueva forma de generar con spawn/3. En lugar de tomar una sola función, spawn/3 toma el módulo, la función y sus argumentos como sus propios argumentos. Una vez que la función se está ejecutando, ocurren los siguientes eventos:
- La función llega a la declaración de recepción. Dado que el buzón del proceso está vacío, nuestro delfín espera hasta que recibe un mensaje;
- Se recibe el mensaje "oh, hello dolphin!". La función intenta hacer coincidir el patrón con do_a_flip. Esto falla, por lo que se intenta el patrón fish y también falla. Finalmente, el mensaje cumple con la cláusula catch-all (_) y coincide.
- El proceso genera el mensaje "Heh, we're smarter than you humans.".
Entonces, debe notarse que si el primer mensaje que enviamos funcionó, el segundo no provocó ninguna reacción del proceso <0.40.0>. Esto se debe al hecho de que una vez que nuestra función generó "Heh, we're smarter than you humans.", finalizó y también lo hizo el proceso. Necesitaremos reiniciar el delfín:
8> f(Dolphin).
ok
9> Dolphin = spawn(dolphins, dolphin1, []).
<0.53.0>
10> Dolphin ! fish.
So long and thanks for all the fish!
fish
Y esta vez el mensaje fish funciona. ¿No sería útil poder recibir una respuesta del delfín en lugar de tener que usar io:format/2? Por supuesto que sí (¿por qué lo pregunto?). La única manera de saber si un proceso ha recibido un mensaje es enviar una respuesta. Nuestro proceso delfín necesitará saber a quién responder. Esto funciona como lo hace con el servicio postal. Si queremos que alguien sepa que debe responder a nuestra carta, necesitamos agregar nuestra dirección. En términos de Erlang, esto se hace empaquetando el pid de un proceso en una tupla. El resultado final es un mensaje que se parece un poco a {Pid, Message}. Creemos una nueva función delfín que acepte dichos mensajes:
dolphin2() ->
receive
{From, do_a_flip} ->
From ! "How about no?";
{From, fish} ->
From ! "So long and thanks for all the fish!";
_ ->
io:format("Heh, we're smarter than you humans.~n")
end.
Como puede ver, en lugar de aceptar do_a_flip y buscar mensajes, ahora necesitamos una variable From. Ahí es donde irá el identificador del proceso.
11> c(dolphins).
{ok,dolphins}
12> Dolphin2 = spawn(dolphins, dolphin2, []).
<0.65.0>
13> Dolphin2 ! {self(), do_a_flip}.
{<0.32.0>,do_a_flip}
14> flush().
Shell got "How about no?"
ok
Parece funcionar bastante bien. Podemos recibir respuestas a los mensajes que enviamos (necesitamos agregar una dirección a cada mensaje), pero aún necesitamos iniciar un nuevo proceso para cada llamada. La recursión es la forma de resolver este problema. Solo necesitamos que la función se llame a sí misma para que nunca finalice y siempre espere más mensajes. Aquí hay una función dolphin3/0 que pone esto en práctica:
dolphin3() ->
receive
{From, do_a_flip} ->
From ! "How about no?",
dolphin3();
{From, fish} ->
From ! "So long and thanks for all the fish!";
_ ->
io:format("Heh, we're smarter than you humans.~n"),
dolphin3()
end.
Aquí, la cláusula catch-all y la cláusula do_a_flip se repiten con la ayuda de dolphin3/0. Tenga en cuenta que la función no hará estallar la pila porque es recursiva de cola. Mientras solo se envíen estos mensajes, el proceso dolphin se repetirá indefinidamente. Sin embargo, si enviamos el mensaje fish, el proceso se detendrá:
15> Dolphin3 = spawn(dolphins, dolphin3, []).
<0.75.0>
16> Dolphin3 ! Dolphin3 ! {self(), do_a_flip}.
{<0.32.0>,do_a_flip}
17> flush().
Shell got "How about no?"
Shell got "How about no?"
ok
18> Dolphin3 ! {self(), unknown_message}.
Heh, we're smarter than you humans.
{<0.32.0>,unknown_message}
19> Dolphin3 ! Dolphin3 ! {self(), fish}.
{<0.32.0>,fish}
20> flush().
Shell got "So long and thanks for all the fish!"
ok
Y eso debería ser todo para dolphins.erl. Como puede ver, respeta nuestro comportamiento esperado de responder una vez por cada mensaje y seguir adelante después, excepto por el llamado con el mensaje fish. El delfín se hartó de nuestras locas payasadas humanas y nos dejó para siempre.