Elixir: séptimo asalto

El aprendizaje va avanzando, ya vamos por el séptimo asalto. Éste trata sobre cómo Elixir maneja múltiples procesos, por lo que trataremos algún tema de concurrencia. También veremos algunas cosas básicas sobre cómo monitorizar los procesos de los que consta nuestra aplicación.

Sin olvidar del método de aprendizaje con el que comenzé:

Seventh

Imagen basada en You gotta fight for your right to... eat de ldjpg, algunos derechos reservados, licencia: CC BY NC ND 2.0

Aprender lo suficiente para comenzar

Trabajando con múltiples procesos

Elixir usa el modelo de actores para gestionar la concurrencia.

Elixir se apoya en Erlang para gestionar los procesos, que no son los procesos del sistema operativo.

Para crear un proceso, se hace con la llamada spawn. spawn puede crear un proceso y ejecutar en él código que tengas en un módulo cualquiera. El proceso puede empezar en cualquier momento (asíncrono total) y se utilizan mensajes entre procesos para sincronizarlos.

Los mensajes no tienen por qué ser Strings, pueden ser de cualquier tipo (generalmente tuplas o atoms). Los mensajes se mandan con send, y debes usar un PID (devuelto por spawn).

El receptor, espera mensajes con receive. receive funciona como case: se pueden poner varios casos, y el primero que coincida, se ejecuta.

receive maneja sólo un mensaje. Si queremos recibir varios, debemos volver a llamar al método que contiene el receive, de forma recursiva (y Elixir es muy bueno con la recursividad). receive también acepta un parámetro, after, para definir un timeout.

El autor dice que los procesos en Elixir son como los objetos en lenguajes orientados a objectos, pero con mejor sentido del humor. El hecho es que son muy livianos, y pueden mantener estado, así que podemos pensar en ellos como en objetos de la programación orientada a objetos.

Enlazar procesos

Normalmente, un proceso no sabe cuando muere un proceso hijo. Debemos hacer algo manualmente para que se notifique. Podemos crear procesos enlazados (linked) con spawn_link. Por defecto, si un proceso hijo muere, mata al proceso padre. Para controlar esto y poder escuchar el mensaje que lanza el proceso hijo al morir, debemos atrapar la salida mediante Process.flag(:trap_exit, true) justo antes de hacer spawn_link.

Dos procesos enlazados pueden comunicarse bidireccionalmente.

Elixir usa el framework OTP para construir árboles de procesos. OTP lleva mucho tiempo en funcionamiento, y debemos confiar en que lo hace mucho mejor que nosotros, por lo que lo usaremos prácticamente siempre. OTP incluye el concepto de Supervisor de procesos. Más adelante estudiaremos temas relacionados con OTP.

Monitorizando procesos

Si spawn_link permite comunicación bidireccional, spawn_monitor solo la permite unidireccional. El proceso hijo puede notificar al padre, pero no al revés.

# monitor devuelve el pid del proceso hijo y una referencia de la monitorización
res = spawn_monitor(<module>, <function>, <params>)
IO.inspect res
# => { #PID{3.3.3.3}, #Reference{1.2.3.4} }

También se puede monitorizar un proceso existente con Process.monitor.

¿Cuándo utilizar cada uno? Depende de la utilidad. Si la muerte de un hijo debería matar al padre, usa procesos enlazados. Si la muerte/fallo de un hijo solamente debería notificar al padre, usa monitorización.

Aprender lo suficiente para hacer algo de utilidad

Resultados

Parece que sí es determinista (al menos con dos procesos). Depende del orden en el que se creen los procesos, incluso si invertimos el orden en el que se envían los tokens, el primer proceso creado es el primero en responder.

Resultados

No recibe ningún mensaje, el hijo termina, terminando al padre durante la espera.

Resultados

No hay mucha difrencia. El padre sigue terminando, sin escuchar ningún mensaje. Al menos, la excepción aparece por consola, mostrándose un error diciendo que el proceso hijo (con su PID) ha lanzado una excepción. En el ejercicio anterior, solamente aparecía que el proceso padre terminaba, nada más.

Usando Process.flag(:trap_exit, true), el proceso padre recibe mensajes:

$ elixir -r exercise-03-round-07.exs -e "Exercise3.run"
Parent's PID #PID<0.48.0>
PID's child #PID<0.53.0>
Received: "Hello!"
Received: {:EXIT, #PID<0.53.0>, :boom}

$ elixir -r exercise-04-round-07.exs -e "Exercise4.run"
Parent's PID #PID<0.48.0>
Child's PID #PID<0.53.0>

22:46:14.313 [error] Process #PID<0.53.0> raised an exception
** (RuntimeError) Child finished
    exercise-04-round-07.exs:19: Exercise4.child/1
Received: "Hello!"
Received: {:EXIT, #PID<0.53.0>, %{RuntimeError{message: "Child finished"},
[{Exercise4, :child, 1, [file: 'exercise-04-round-07.exs', line: 19]}]}}

Las diferencias están en lo recibido en el mensaje de terminación del hijo. En caso de exit se recibe :EXIT, un PID, y la causa de la salida. En el caso de la excepción: :EXIT, un PID y la excepción, parece, porque tiene pinta de pila de llamadas, con su módulo, función, parámetros,…

Resultados

No creo que lo esté haciendo bien. Se supone que monitorizando la comunicación no es bidireccional, pero el padre recibe el mensaje que envía el hijo, así como el mensaje que se envía al terminar o lanzar la excepción. La única diferencia visible es que en lugar de recibir solamente un PID, se recibe un PID y la referencia de monitorización.

Resultados

Aumentando el número de elementos afecta al orden en el que se reciben los mensajes. También he conseguido recibir mensajes en orden distinto con el siguiente código:

Parallel.pmap 1..10, fn (i) ->
  # la espera es más corta según el elemento `i` se va a haciendo mayor
  wait_up_to = round(10 / i)
  :timer.sleep(wait_up_to)
  i
end
  #=> [ 7, 8, 9, 10, 5, 6, 3, 4, 2, 1 ]

Volviendo a poner ^pid el orden vuelve a ser correcto.

Enseñar lo aprendido, y repetir desde el paso 7

Aquí está, este post, mis notas, mis pensamientos, mis dudas y mi código. Hasta el siguiente asalto.