Elixir - Processos

No Elixir, todo código é executado dentro de processos. Os processos são isolados uns dos outros, são executados simultaneamente entre si e se comunicam por meio da passagem de mensagens. Os processos do Elixir não devem ser confundidos com os processos do sistema operacional. Os processos no Elixir são extremamente leves em termos de memória e CPU (ao contrário dos threads em muitas outras linguagens de programação). Por causa disso, não é incomum ter dezenas ou mesmo centenas de milhares de processos em execução simultaneamente.

Neste capítulo, aprenderemos sobre as construções básicas para gerar novos processos, bem como enviar e receber mensagens entre diferentes processos.

A Função de Spawn

A maneira mais fácil de criar um novo processo é usar o spawnfunção. ospawnaceita uma função que será executada no novo processo. Por exemplo -

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

Quando o programa acima é executado, ele produz o seguinte resultado -

false

O valor de retorno da função spawn é um PID. Este é um identificador único para o processo e, portanto, se você executar o código acima do seu PID, ele será diferente. Como você pode ver neste exemplo, o processo está morto quando verificamos se ele está vivo. Isso ocorre porque o processo será encerrado assim que terminar de executar a função fornecida.

Como já mencionado, todos os códigos Elixir são executados dentro de processos. Se você executar a função própria, verá o PID para a sua sessão atual -

pid = self
 
Process.alive?(pid)

Quando o programa acima é executado, ele produz o seguinte resultado -

true

Passagem de mensagens

Podemos enviar mensagens para um processo com send e recebê-los com receive. Vamos passar uma mensagem para o processo atual e recebê-la no mesmo.

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

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

Quando o programa acima é executado, ele produz o seguinte resultado -

Hi people

Enviamos uma mensagem para o processo atual usando a função send e a passamos para o próprio PID. Em seguida, lidamos com a mensagem recebida usando oreceive função.

Quando uma mensagem é enviada para um processo, a mensagem é armazenada no process mailbox. O bloco de recebimento passa pela caixa de correio do processo atual em busca de uma mensagem que corresponda a qualquer um dos padrões fornecidos. O bloco de recebimento oferece suporte a guardas e muitas cláusulas, como o caso.

Se não houver nenhuma mensagem na caixa de correio que corresponda a qualquer um dos padrões, o processo atual aguardará até que uma mensagem correspondente chegue. Um tempo limite também pode ser especificado. Por exemplo,

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

Quando o programa acima é executado, ele produz o seguinte resultado -

nothing after 1s

NOTE - Um tempo limite de 0 pode ser fornecido quando você já espera que a mensagem esteja na caixa de correio.

Links

A forma mais comum de desova no Elixir é via spawn_linkfunção. Antes de dar uma olhada em um exemplo com spawn_link, vamos entender o que acontece quando um processo falha.

spawn fn -> raise "oops" end

Quando o programa acima é executado, ele produz o seguinte erro -

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

Ele registrou um erro, mas o processo de geração ainda está em execução. Isso ocorre porque os processos são isolados. Se quisermos que a falha de um processo se propague para outro, precisamos vinculá-los. Isso pode ser feito com ospawn_linkfunção. Vamos considerar um exemplo para entender o mesmo -

spawn_link fn -> raise "oops" end

Quando o programa acima é executado, ele produz o seguinte erro -

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

Se você está executando isto em iexo shell trata desse erro e não fecha. Mas se você executar primeiro fazendo um arquivo de script e, em seguida, usandoelixir <file-name>.exs, o processo pai também será desativado devido a essa falha.

Processos e links desempenham um papel importante na construção de sistemas tolerantes a falhas. Em aplicativos Elixir, frequentemente vinculamos nossos processos a supervisores que detectam quando um processo morre e iniciam um novo processo em seu lugar. Isso só é possível porque os processos são isolados e não compartilham nada por padrão. E, como os processos são isolados, não há como uma falha em um processo travar ou corromper o estado de outro. Enquanto outras linguagens requerem que capturemos / tratemos exceções; no Elixir, não há problema em deixar os processos falharem porque esperamos que os supervisores reiniciem nossos sistemas de maneira adequada.

Estado

Se você estiver criando um aplicativo que requer estado, por exemplo, para manter a configuração do aplicativo, ou precisar analisar um arquivo e mantê-lo na memória, onde o armazenaria? A funcionalidade de processo do Elixir pode ser útil ao fazer essas coisas.

Podemos escrever processos com loop infinito, manter o estado e enviar e receber mensagens. Como exemplo, vamos escrever um módulo que inicia novos processos que funcionam como um armazenamento de valor-chave em um arquivo chamadokv.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

Observe que o start_link função inicia um novo processo que executa o loopfunção, começando com um mapa vazio. oloopa função então espera por mensagens e executa a ação apropriada para cada mensagem. No caso de um:getmensagem, ele envia uma mensagem de volta para o chamador e faz um loop de chamadas novamente, para esperar por uma nova mensagem. Enquanto o:put a mensagem realmente invoca loop com uma nova versão do mapa, com a chave e o valor fornecidos armazenados.

Vamos agora executar o seguinte -

iex kv.exs

Agora você deve estar em seu iexConcha. Para testar nosso módulo, tente o seguinte -

{: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()

Quando o programa acima é executado, ele produz o seguinte resultado -

"Hello"