Elixir - Procesos

En Elixir, todo el código se ejecuta dentro de los procesos. Los procesos están aislados entre sí, se ejecutan de forma simultánea entre sí y se comunican mediante el paso de mensajes. Los procesos de Elixir no deben confundirse con los procesos del sistema operativo. Los procesos en Elixir son extremadamente livianos en términos de memoria y CPU (a diferencia de los subprocesos en muchos otros lenguajes de programación). Debido a esto, no es raro tener decenas o incluso cientos de miles de procesos ejecutándose simultáneamente.

En este capítulo, aprenderemos sobre las construcciones básicas para generar nuevos procesos, así como para enviar y recibir mensajes entre diferentes procesos.

La función de generación

La forma más sencilla de crear un nuevo proceso es utilizar el spawnfunción. losspawnacepta una función que se ejecutará en el nuevo proceso. Por ejemplo

pid = spawn(fn -> 2 * 2 end)
Process.alive?(pid)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

false

El valor de retorno de la función de generación es un PID. Este es un identificador único para el proceso y, por lo tanto, si ejecuta el código sobre su PID, será diferente. Como puede ver en este ejemplo, el proceso está muerto cuando verificamos si está vivo. Esto se debe a que el proceso se cerrará tan pronto como haya terminado de ejecutar la función dada.

Como ya se mencionó, todos los códigos de Elixir se ejecutan dentro de los procesos. Si ejecuta la función self, verá el PID de su sesión actual:

pid = self
 
Process.alive?(pid)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

true

Paso de mensajes

Podemos enviar mensajes a un proceso con send y recibirlos con receive. Pasemos un mensaje al proceso actual y recibamos en el mismo.

send(self(), {:hello, "Hi people"})

receive do
   {:hello, msg} -> IO.puts(msg)
   {:another_case, msg} -> IO.puts("This one won't match!")
end

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

Hi people

Enviamos un mensaje al proceso actual usando la función de envío y lo pasamos al PID de self. Luego manejamos el mensaje entrante usando elreceive función.

Cuando se envía un mensaje a un proceso, el mensaje se almacena en el process mailbox. El bloque de recepción pasa por el buzón del proceso actual en busca de un mensaje que coincida con cualquiera de los patrones dados. El bloque de recepción admite guardias y muchas cláusulas, como case.

Si no hay ningún mensaje en el buzón que coincida con ninguno de los patrones, el proceso actual esperará hasta que llegue un mensaje coincidente. También se puede especificar un tiempo de espera. Por ejemplo,

receive do
   {:hello, msg}  -> msg
after
   1_000 -> "nothing after 1s"
end

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

nothing after 1s

NOTE - Se puede dar un tiempo de espera de 0 cuando ya espera que el mensaje esté en el buzón.

Enlaces

La forma más común de desove en Elixir es en realidad a través de spawn_linkfunción. Antes de echar un vistazo a un ejemplo con spawn_link, comprendamos qué sucede cuando falla un proceso.

spawn fn -> raise "oops" end

Cuando se ejecuta el programa anterior, produce el siguiente error:

[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
   :erlang.apply/2

Se registró un error, pero el proceso de generación aún se está ejecutando. Esto se debe a que los procesos están aislados. Si queremos que la falla en un proceso se propague a otro, debemos vincularlos. Esto se puede hacer conspawn_linkfunción. Consideremos un ejemplo para entender lo mismo:

spawn_link fn -> raise "oops" end

Cuando se ejecuta el programa anterior, produce el siguiente error:

** (EXIT from #PID<0.41.0>) an exception was raised:
   ** (RuntimeError) oops
      :erlang.apply/2

Si está ejecutando esto en iexshell entonces el shell maneja este error y no sale. Pero si ejecuta primero haciendo un archivo de script y luego usandoelixir <file-name>.exs, el proceso principal también se desactivará debido a esta falla.

Los procesos y enlaces juegan un papel importante en la construcción de sistemas tolerantes a fallas. En las aplicaciones de Elixir, a menudo vinculamos nuestros procesos a supervisores que detectarán cuando un proceso muere y comenzarán un nuevo proceso en su lugar. Esto solo es posible porque los procesos están aislados y no comparten nada de forma predeterminada. Y dado que los procesos están aislados, no hay forma de que una falla en un proceso bloquee o corrompa el estado de otro. Mientras que otros lenguajes requerirán que capturemos / manejemos excepciones; en Elixir, estamos realmente bien dejando que los procesos fallen porque esperamos que los supervisores reinicien correctamente nuestros sistemas.

Estado

Si está creando una aplicación que requiere estado, por ejemplo, para mantener la configuración de su aplicación, o necesita analizar un archivo y guardarlo en la memoria, ¿dónde lo almacenaría? La funcionalidad del proceso de Elixir puede resultar útil al hacer tales cosas.

Podemos escribir procesos que se repiten infinitamente, mantener el estado y enviar y recibir mensajes. Como ejemplo, escribamos un módulo que inicie nuevos procesos que funcionen como un almacén de clave-valor en un archivo llamadokv.exs.

defmodule KV do
   def start_link do
      Task.start_link(fn -> loop(%{}) end)
   end

   defp loop(map) do
      receive do
         {:get, key, caller} ->
         send caller, Map.get(map, key)
         loop(map)
         {:put, key, value} ->
         loop(Map.put(map, key, value))
      end
   end
end

Tenga en cuenta que el start_link La función inicia un nuevo proceso que ejecuta el loopfunción, comenzando con un mapa vacío. losloopLa función luego espera mensajes y realiza la acción apropiada para cada mensaje. En el caso de un:getmensaje, envía un mensaje de vuelta a la persona que llama y las llamadas se repiten para esperar un nuevo mensaje. Mientras que la:put el mensaje realmente invoca loop con una nueva versión del mapa, con la clave y el valor dados almacenados.

Ejecutemos ahora lo siguiente:

iex kv.exs

Ahora deberías estar en tu iexcáscara. Para probar nuestro módulo, intente lo siguiente:

{:ok, pid} = KV.start_link

# pid now has the pid of our new process that is being 
# used to get and store key value pairs 

# Send a KV pair :hello, "Hello" to the process
send pid, {:put, :hello, "Hello"}

# Ask for the key :hello
send pid, {:get, :hello, self()}

# Print all the received messages on the current process.
flush()

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

"Hello"