DEV Community

Cover image for Processos em Elixir
Willian Frantz
Willian Frantz

Posted on • Edited on

Processos em Elixir

Para quem é este texto?

Este texto vai ser um pouco mais teórico, então se você está buscando entender melhor sobre como os processos em Elixir funcionam, continue lendo...

Por que estudar processos?

Como diria Joe Armstrong, um dos criadores do Erlang:

"O Erlang foi projetado desde o começo para ser uma linguagem de programação funcional, que permitisse a criação de programas concorrentes e distribuídos." - citado em Programming Erlang, Software for a concurrent world.

E assim como Erlang, o Elixir também possui essas mesmas características, e lidar com programas concorrentes tem se tornado cada vez mais uma necessidade real no mundo do Desenvolvimento de Software.

Por isso, lidar com processos e suas abstrações no Elixir é um tópico extremamente importante, não só para conseguirmos aprimorar os nossos projetos, mas também para entender como a linguagem, frameworks e bibliotecas disponíveis nesse ecossistema funcionam.

Por onde começar?

Elixir atualmente possui diversos tipos de abstrações de processos, como por exemplo: Tasks, Agents, GenServers e Supervisors... Onde cada abstração possui a sua própria responsabilidade definida.

É importante estar por dentro deste mundo de abstrações de processos desde a parte mais básica - utilizando spawn/receive - até a criação de processos com GenServers e Supervisors, já que estamos falando sobre uma linguagem de programação concorrente.

Supondo que precisamos criar um módulo que será responsável por tratar detalhes específicos da funcionalidade do nosso produto, que não necessariamente precisam ser executados de forma síncrona, como um disparador de emails, ou até mesmo um disparador de tokens para um sistema 2FA (2 Factor Authentication).

Ambos são exemplos de aplicações boas para ter um processo secundário rodando.

Mas como eu posso começar a trabalhar com processos?

Iniciando com spawn/1

Uma das formas que tem funcionado muito bem para mim tem sido tentar entender como funcionavam os processos em Elixir desde o mais básico, utilizando somente spawn/1.

Dito isso, vamos começar com um exemplo:

Digamos que eu precise criar um processo que irá esperar uma mensagem e disparar um IO.inspect/1 (Função de inspecionar o elemento) com os argumentos recebidos.

# módulo que irá disparar as mensagens
defmodule DispatchMessage do
  @moduledoc """
  Process that will await for a message, dispatch when
  received and terminate. 
  """

  @spec start_link() :: pid()
  def start_link() do
    spawn(&wait_message/0)
  end

  defp wait_message() do
    receive do
      {:inspect, data} -> IO.inspect(data)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Ele irá executar uma simples atividade de forma assíncrona e encerrar.

Podemos checar isso da seguinte forma:

iex> pid = DispatchMessage.start_link()
#PID<0.119.0>

iex> Process.alive? pid
true

iex> send pid, {:inspect, %{hello: :world}}
%{hello: :world}

iex> Process.alive? pid
false
Enter fullscreen mode Exit fullscreen mode

Podemos ver com o Process.alive?/1 que o processo foi encerrado após executar a sua task de IO.inspect(data).

Mas o que exatamente aconteceu quando executamos este trecho de código no iex?

O DispatchMessage.start_link/0 retornou um PID (Process ID), que é o que usaremos para nos comunicar com o processo que acabamos de criar.

Já o send/2 é a função que usamos para enviar uma mensagem para o processo (PID) que criamos. Este mesmo processo já estava esperando receber uma mensagem a partir do trecho: wait_message/0.

Ciclo de vida

Existem alguns detalhes importantes para mencionar sobre o ciclo de vida desses tipos de processos.

  • Os processos criados a partir do spawn/1 por padrão irão executar uma task simples e encerrar.
  • Apesar de estarmos paralelizando o projeto utilizando processos, nosso processo só será capaz de executar um comando por vez.
  • Chamadas em excesso podem fazer com que o processo precise enfileirar as demais chamadas e isso pode causar timeout em chamadas síncronas (pode acontecer se você estiver usando GenServer handle_call/3).
  • Podemos determinar se esse processo está esperando por um input utilizando a estrutura de receive.
iex> pid = spawn(fn -> 
...>   receive do 
...>     {:msg, msg} -> IO.puts("Recebi uma mensagem #{msg}")
...>   end
...> end)

iex> send pid, {:msg, "oi"}
"Recebi uma mensagem oi"
Enter fullscreen mode Exit fullscreen mode

Podemos definir também loops e até mesmo simular um estado interno para processos desse tipo, por exemplo:

defmodule StatefulProcess do
  @moduledoc """
  Stateful process abstraction using spawn/1
  """

  @spec start_link() :: pid()
  def start_link() do
    spawn(&loop/0)
  end

  @spec add_data(pid(), map()) :: tuple()
  def add_data(pid, data) do
    send pid, {:msg, data}
  end

  @spec list(pid()) :: atom()
  def list(pid), do: send pid, :list

  defp loop(), do: loop([])
  defp loop(state) do
    receive do
      {:msg, map_msg} ->
        state ++ [map_msg]
      :list ->
        IO.inspect(state)
        state
    end
    |> loop()
  end
end
Enter fullscreen mode Exit fullscreen mode

Acabamos de criar um módulo de processo com estado interno que irá iniciar vazio e incrementará a lista de estado a partir das chamadas add_data/2

ex:

iex> pid = StatefulProcess.start_link
#PID<0.231.0>

iex> StatefulProcess.add_data pid, %{hello: :world}
{:msg, %{hello: :world}}

iex> StatefulProcess.list(pid)
[%{hello: :world}]
:list
Enter fullscreen mode Exit fullscreen mode

A grande sacada deste novo módulo é que quando iniciarmos o processo pelo c:start_link/0, o mesmo não irá terminar após alguma execução (seja ela: c:add_data/2 ou c:list/1), além de que ele também irá armazenar o estado atualizado a partir do c:add_data/2.

Conclusão

Perceba a quantidade de possibilidades que temos somente com a implementação mais básica de processos em Elixir: imagine o que podemos fazer usando as demais abstrações acima!?

A ideia deste texto era desmistificar processos com Elixir, mostrar algumas abordagens usando spawn/1 e receive-do e destacar alguns pontos importantes sobre ciclo de vida dentro da linguagem.

Os tópicos abordados neste texto podem servir para os demais tipos de processos. Pretendo escrever mais sobre as outras abstrações!

Até mais...

cat bye giphy

Top comments (3)

Collapse
 
elixir_utfpr profile image
Elixir UTFPR (por Adolfo Neto) • Edited

Fiz um vídeo sobre este texto: youtu.be/rTRtUE6lgpI

Collapse
 
wlsf profile image
Willian Frantz

O video ficou excelente, achei extremamente didático a forma como você passa pelo conteúdo e vai complementando.

Muito obrigado por compartilhar!!!

Collapse
 
pedromcorreia profile image
Pedro Correia

Seria interessante explicar como funcionam os processos, comunicação entre processos, mailboxes, do resto muito bom conteudo