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
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
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"
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
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
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...
Top comments (3)
Fiz um vídeo sobre este texto: youtu.be/rTRtUE6lgpI
O video ficou excelente, achei extremamente didático a forma como você passa pelo conteúdo e vai complementando.
Muito obrigado por compartilhar!!!
Seria interessante explicar como funcionam os processos, comunicação entre processos, mailboxes, do resto muito bom conteudo