Processos¶
Módulo 1: Fundamentos de Computação Concorrente na BEAM¶
Objetivo: Compreender o modelo de execução da Máquina Virtual Erlang (BEAM) e contrastá-lo com os modelos tradicionais de Threads de SO e Green Threads (Go).
1. O Problema da Concorrência Tradicional
Na engenharia de software clássica (Java, C++, C#), a unidade de concorrência é a Thread do Sistema Operacional (OS Thread).
• Peso: Cada thread consome uma quantidade significativa de memória (geralmente alguns MBs apenas para a stack).
• Gerenciamento: O Kernel do SO é responsável por agendar essas threads (Context Switching). Trocar de contexto no nível do kernel é uma operação custosa (latência).
• Memória Compartilhada: Threads do mesmo processo compartilham o mesmo espaço de memória (Heap).
◦ O Perigo: Se a Thread A e a Thread B tentam escrever na variável x ao mesmo tempo, ocorre uma Condição de Corrida (Race Condition).
◦ A Solução (e o problema dela): Usamos Mutexes, Semáforos e Locks. Isso introduz complexidade e riscos de Deadlocks.
2. A Solução Elixir: O Modelo de Atores e a BEAM
O Elixir roda sobre a BEAM. A BEAM não utiliza threads do SO diretamente para executar sua lógica. Ela utiliza Processos Leves (Lightweight Processes).
2.1. O Processo da BEAM
• Peso: Extremamente leve (inicia com cerca de ~300 words ou ~2.5KB). Você pode rodar milhões deles em um laptop. • Isolamento de Memória (Share Nothing): Cada processo tem sua própria Heap e Stack. O Processo A não consegue acessar a memória do Processo B. • Comunicação: A única forma de interação é através de Troca de Mensagens (Message Passing). Os dados são copiados de um processo para outro (com exceção de binários grandes, que usam contagem de referência).
2.2. O Scheduler da BEAM (Escalonamento)
Isso é crucial para engenheiros: A BEAM roda, geralmente, uma Thread de SO por Núcleo de CPU. Dentro dessas threads, a BEAM roda seus próprios escalonadores (Schedulers). • Escalonamento Preemptivo: A BEAM atribui a cada processo um número de "reduções" (aproximadamente 2000 chamadas de função). Quando as reduções acabam, a BEAM pausa o processo forçadamente e dá a vez para o próximo. ◦ Vantagem: Um processo com loop infinito ou cálculo pesado não trava o sistema. Em Node.js (single-thread event loop), um cálculo pesado trava tudo. No Elixir, não.
3. Comparativo Técnico: Elixir vs. Go (Goroutines) vs. Java (Threads)
Esta é uma dúvida comum de mercado e academia.
| Característica | Java / C# (OS Threads) | Go (Goroutines) | Elixir (BEAM Processes) |
|---|---|---|---|
| Gerenciamento | Kernel do SO | Runtime da linguagem (M:N) | Runtime da linguagem (M:N) |
| Uso de Memória | Alto (MBs) | Baixo (KBs) | Muito Baixo (KBs) |
| Memória | Compartilhada (precisa de mutex) | Compartilhada (mas encoraja channels) | Isolada — Share Nothing |
| Tolerância a Falhas | Se uma thread crashar, o processo pode cair. | Se houver panic não tratado, o programa cai. | Se um processo crashar, só ele morre. O sistema continua. |
| Latência | Previsível (limitada pelo SO) | Baixa | Soft Real-Time (latência muito previsível) |
Nota para o Engenheiro¶
Goroutines (Go) e Processos Elixir (BEAM) são ambos green threads, mas com filosofias opostas:
- Go: paralelismo com memória compartilhada, embora incentive channels.
- Elixir/BEAM: isolamento total, focado em tolerância a falhas e sistemas auto-curáveis.
4. Conceitos Fundamentais
Precisamos alinhar o vocabulário para as próximas etapas.
4.1. Concorrência vs. Paralelismo
• Concorrência: É sobre a estrutura do programa. É a capacidade de lidar com muitas coisas ao mesmo tempo. (Ex: Um servidor web aceitando 10 mil conexões). • Paralelismo: É sobre a execução física. É fazer muitas coisas no mesmo instante de tempo. Requer múltiplos núcleos de CPU. ◦ Em Elixir: Você escreve código concorrente (muitos processos). A BEAM se encarrega de paralelizar isso automaticamente em todos os núcleos disponíveis.
4.2. Síncrono vs. Assíncrono
Em sistemas distribuídos (e no Elixir): • Síncrono (Call): Envio a mensagem e bloqueio minha execução até receber a resposta (Request/Response). • Assíncrono (Cast): Envio a mensagem e sigo minha vida ("Fire and forget"). Não sei se a mensagem chegou ou se foi processada na hora.
4.3. Transparência de Localização (Sistemas Distribuídos)
Como processos se comunicam apenas por mensagens enviadas para um endereço (PID - Process ID), não importa onde esse processo está.
• Enviar mensagem para o PID <0.100.0> (na mesma máquina).
• Enviar mensagem para o PID <50.100.0> (em um servidor no Japão).
O código é exatamente o mesmo. Isso torna o Elixir uma linguagem naturalmente orientada a sistemas distribuídos (Clusters).
Módulo 2: Laboratório de Anatomia de Processos¶
Objetivo: Manipular as primitivas de concorrência da BEAM (spawn, send, receive) e entender o ciclo de vida de um processo.
1. Preparação do Ambiente¶
Abra seu terminal. Vamos criar um projeto chamado lab_concorrencia.
Isso carrega o ambiente do Mix, que será útil para compilar os módulos que criaremos mais à frente.
O comando iex -S mix abre o shell interativo carregando o contexto do seu projeto.
2. Identidade: O PID (Process Identifier)¶
Em sistemas distribuídos, para enviar uma mensagem, precisamos de um endereço. No Elixir, esse endereço é o PID.
No IEx, digite:
Saída esperada: #PID<0.152.0> (Os números podem variar).
Análise do Professor:
- Este é o endereço do próprio terminal IEx. Sim, o shell onde você digita é um Processo Elixir rodando sobre a BEAM.
- O formato
<A.B.C>geralmente indica:A: Nó (Node) onde o processo está rodando (0 = máquina local).B: ID sequencial do processo.C: Serial (usado quando o B "dá a volta" no contador).
3. Criação: spawn (Vida e Morte)¶
A função spawn/1 aceita uma função anônima, cria um processo novo para executá-la em uma stack de memória isolada e retorna o PID dele.
Digite no IEx:
Agora, verifique se esse processo ainda existe:
O Conceito de Ciclo de Vida: Por que ele morreu? Diferente de uma Thread Java que pode ficar "idle" (ociosa) gastando recursos, um Processo Elixir é estritamente funcional.
- Ele nasce.
- Ele executa a função.
- Se não há mais instruções, ele termina (exit normal).
- O Garbage Collector limpa a memória dele imediatamente.
4. Comunicação Assíncrona: send e a Mailbox¶
Todo processo nasce com uma Caixa de Correio (Mailbox) interna. O envio de mensagens em Elixir é Assíncrono (Non-blocking).
Vamos usar o seu próprio processo do IEx (self()) para testar.
-
Armazene seu endereço:Elixir
-
Envie uma mensagem para si mesmo:Elixir
Observe: A função retorna a mensagem enviada, mas nada foi impresso na tela como "Recebido". Onde está a mensagem?
-
Inspecione a memória do processo (Buffer): Use a função de debug
flush():Elixir
Análise do Engenheiro:
A mensagem ficou armazenada na Heap do processo. O send apenas deposita a mensagem e retorna o controle imediatamente para a CPU. O remetente não sabe se a mensagem foi lida. Isso é o fundamento do desacoplamento em sistemas distribuídos.
5. Sincronização: receive (O Bloqueio)¶
Para ler a mensagem, o processo precisa parar o que está fazendo e verificar a caixa de correio. Isso é feito com receive.
Atenção: O receive é uma operação bloqueante. Se a caixa estiver vazia, o processo dorme (não gasta CPU) até chegar algo.
Vamos criar um processo que sabe esperar por uma mensagem.
Digite no IEx:
# Definimos a função que o processo vai rodar
funcao_ouvinte = fn ->
receive do
{:ping, remetente_pid} ->
IO.puts "🏓 Recebi PING. Enviando PONG..."
send(remetente_pid, :pong)
end
end
# Criamos o processo
pid_ping = spawn(funcao_ouvinte)
Verifique se ele está vivo:
Ele está vivo, parado na linha do receive, aguardando.
Agora, vamos interagir:
send(pid_ping, {:ping, self()})
# Saída no terminal: 🏓 Recebi PING. Enviando PONG...
# Retorno: {:ping, #PID<...>}
Verifique se recebemos a resposta (:pong):
Verifique se o processo ouvinte ainda vive:
Ele processou o ping, acabou o bloco receive, saiu da função e morreu.
6. A Persistência: Recursão de Cauda (O Servidor)¶
Para criar um sistema (como um Banco ou Webserver), o processo não pode morrer após uma mensagem. Ele deve voltar ao estado de espera.
Em linguagens imperativas, usaríamos while(true). Em Elixir, usamos Recursão.
Como estamos dentro do projeto lab_concorrencia, vamos criar um arquivo real para isso.
- Abra seu editor (VS Code).
- Crie o arquivo
lib/processo_persistente.ex.
defmodule ProcessoPersistente do
def aguardar_mensagem do
receive do
{:mensagem, texto} ->
IO.puts "📝 Log: #{texto}"
# AQUI ESTÁ O SEGREDO:
# Chamamos a função novamente. O processo volta para o topo e para no 'receive'.
aguardar_mensagem()
{:parar} ->
IO.puts "🛑 Encerrando processo..."
# Não chamamos a recursão. O processo chegará ao fim e morrerá.
end
end
end
Recompile o projeto para ler o arquivo novo:
ou caso tenha saido:
Teste a persistência:
-
Entendendo o
spawn/3Quando usamos
spawn/3, estamos usando o padrão MFA (Module, Function, Arguments), que é onipresente na máquina virtual do Elixir.O terceiro parâmetro (
[]) é a Lista de Argumentos Iniciais que serão passados para a função quando ela começar.1. Por que está vazio
[]esse exemplo?¶Olhe para a definição da função que criamos:
Como a função
aguardar_mensagem/0não pede nenhum dado para começar, nós passamos uma lista vazia[]nospawn.2. E se a função precisasse de dados?¶
Imagine que queremos criar um processo que já nasce sabendo o nome do usuário.
A função seria:
O Spawn seria:
Agora vamos enviar algumas mensagens:
# 2. Enviar múltiplas mensagens
send(pid, {:mensagem, "Primeiro log"})
send(pid, {:mensagem, "Segundo log"})
send(pid, {:mensagem, "Terceiro log"})
# 3. Verificar vida
Process.alive?(pid)
# true (Ele processou tudo e voltou a dormir no receive)
# 4. Parar
send(pid, {:parar})
Process.alive?(pid)
# false
Professor: Finalizamos o laboratório de primitivas. O aluno aprendeu:
- Como criar um PID (
spawn). - Que o envio é "Fire-and-forget" (
send). - Que o recebimento trava o processo (
receive). - Que servidores são apenas loops recursivos infinitos.
Módulo 3: Construindo um Servidor de Conta¶
Objetivo: Implementar um processo com estado persistente (Stateful). Faremos isso em duas etapas:
- A Versão "Raiz" (Manual): Para entender onde o estado mora (recursão).
- A Versão "Profissional" (GenServer): Para ganhar robustez e padronização.
1. Etapa A: O Servidor Manual (Entendendo o Loop)¶
Antes de usarmos ferramentas prontas, vamos criar uma conta bancária usando apenas primitivas: spawn, receive e recursão. Isso vai provar para você que não existe mágica, apenas engenharia.
Passo 1: O Loop de Estado¶
Crie o arquivo lib/conta_manual.ex.
Neste código, observe que o saldo não é uma variável global. Ele é um argumento que é passado de uma volta do loop para a próxima.
defmodule ContaManual do
# Função para iniciar o processo
def iniciar(saldo_inicial) do
spawn(__MODULE__, :loop_do_servidor, [saldo_inicial])
end
# O CORAÇÃO DO PROCESSO
# Esta função nunca termina. Ela fica girando eternamente.
def loop_do_servidor(saldo_atual) do
# 1. O processo para e espera uma mensagem chegar na caixa de correio
receive do
{:depositar, valor} ->
novo_saldo = saldo_atual + valor
IO.puts "💰 [Manual] Depositado #{valor}. Novo saldo: #{novo_saldo}"
# A MÁGICA: Reinicia o loop, passando o NOVO saldo
loop_do_servidor(novo_saldo)
{:ver_saldo, pid_do_cliente} ->
# Para responder, precisamos enviar uma mensagem de volta manualmente
send(pid_do_cliente, {:resposta_saldo, saldo_atual})
# Reinicia o loop mantendo o MESMO saldo
loop_do_servidor(saldo_atual)
end
end
end
Testando a Versão Manual (IEx)¶
Abra o terminal e veja a dificuldade de comunicação:
# 1. Iniciar
pid = ContaManual.iniciar(0)
# 2. Depositar (Fácil, só enviar)
send(pid, {:depositar, 500})
# "💰 [Manual] Depositado 500. Novo saldo: 500"
# 3. Ver Saldo (Difícil! Preciso enviar meu PID e esperar resposta)
send(pid, {:ver_saldo, self()})
# Agora preciso caçar a resposta na minha caixa de correio
flush()
# {:resposta_saldo, 500}
Análise do Professor: Funciona? Sim. Mas é frágil.
- E se o processo morrer? O cliente fica esperando pra sempre.
- E se eu quiser esperar a resposta (Síncrono)? Teria que escrever um
receiveno cliente. - É muito código repetitivo ("Boilerplate").
2. Etapa B: O Padrão Ouro (GenServer)¶
Na Etapa A, construímos um servidor "na unha". Funciona? Sim. É seguro para produção? Não. Como engenheiros, analisamos os riscos daquela abordagem manual:
- Boilerplate: Você teve que escrever a recursão (
loop_do_servidor) manualmente. Se esquecer uma linha, o servidor morre. - Timeouts: Se você mandar uma mensagem e o servidor não responder, seu cliente fica travado para sempre.
- Padronização: Se cada programador escrever seu próprio loop
receive, o código do projeto vira uma bagunça imprevisível. Para resolver isso, a OTP (Open Telecom Platform) nos dá o GenServer (Generic Server). Ele abstrai a recursão, o tratamento de erros e o ciclo de vida do processo.
2.1. Teoria: A Fronteira do Processo (Client vs Server)
A maior confusão de quem começa com GenServer é entender onde o código está rodando. O módulo do GenServer é dividido em duas partes que rodam em processos diferentes. Imagine um Restaurante:
- Client API (O Garçom): É a interface pública. Você chama essas funções. Elas rodam no seu processo (no terminal ou na requisição HTTP). O trabalho delas é apenas anotar o pedido e gritar para a cozinha.
- Server Callbacks (A Cozinha): É a implementação interna. Essas funções rodam dentro do processo do GenServer. Elas recebem o pedido, cozinham (processam a lógica/estado) e devolvem o prato.
2.2. Os Protocolos de Comunicação (Call vs Cast)
Em sistemas distribuídos, a forma como enviamos mensagens define a confiabilidade do sistema. O GenServer padroniza isso em dois verbos:
| Tipo | O Verbo | Analogia do Mundo Real | Comportamento Técnico |
|---|---|---|---|
| Síncrono | Call | Ligar para a Pizzaria | Você fica esperando a resposta. Se não atender em 5s (timeout), gera erro. Garante consistência. |
| Assíncrono | Cast | Mandar um e-mail | Você envia e não espera retorno. Pode ser lido agora ou mais tarde. Garante performance (“fire and forget”). |
3. Laboratório Prático: O Banco Profissional
Agora vamos refazer nossa conta bancária, mas usando a estrutura que você verá em empresas reais como Discord ou WhatsApp.
Desafio de Engenharia:
Implementar o módulo ContaBancaria onde:
• O Estado (Saldo) é gerenciado automaticamente pelo GenServer.
• Depósitos usam cast (pois o banco aceita seu dinheiro na hora, sem bloquear).
• Saques e Consultas usam call (pois você precisa da resposta imediata).
Passo 2: A Implementação Profissional¶
Vamos criar o módulo ContaBancaria (a versão final) dentro do projeto.
Crie o arquivo lib/conta_bancaria.ex.
Note como loop(novo_saldo) vira {:noreply, novo_saldo}.
Elixir
defmodule ContaBancaria do
# 1. Injeta o comportamento de GenServer (o loop invisível)
use GenServer
# ===================================================================
# API DO CLIENTE (Roda no processo do usuário)
# ===================================================================
@doc "Inicia a conta. O argumento é o saldo inicial (default 0)."
def abrir_conta(saldo_inicial \\ 0) do
# __MODULE__ é um atalho para 'ContaBancaria'.
GenServer.start_link(__MODULE__, saldo_inicial)
end
def ver_saldo(pid) do
# Call = Síncrono. O GenServer cuida de enviar, esperar e retornar o valor.
GenServer.call(pid, :mostrar_saldo)
end
def depositar(pid, valor) do
# Cast = Assíncrono. Só avisa.
GenServer.cast(pid, {:depositar, valor})
end
def sacar(pid, valor) do
# Call = Síncrono. Preciso saber se deu certo.
GenServer.call(pid, {:sacar, valor})
end
# ===================================================================
# CALLBACKS DO SERVIDOR (A Lógica Interna)
# ===================================================================
# 2. init/1: Define o estado inicial da primeira volta do loop.
@impl true
def init(saldo_inicial) do
{:ok, saldo_inicial}
end
# 3. handle_call: Responde ao cliente E decide o estado futuro.
@impl true
def handle_call(:mostrar_saldo, _from, saldo_atual) do
# {:reply, O_QUE_RESPONDE, O_ESTADO_FUTURO}
{:reply, saldo_atual, saldo_atual}
end
@impl true
def handle_call({:sacar, valor}, _from, saldo_atual) do
if saldo_atual >= valor do
novo_saldo = saldo_atual - valor
{:reply, {:ok, novo_saldo}, novo_saldo}
else
# Responde erro e MANTÉM o estado antigo
{:reply, {:error, "Saldo Insuficiente"}, saldo_atual}
end
end
# 4. handle_cast: Não responde nada, apenas atualiza o estado futuro.
@impl true
def handle_cast({:depositar, valor}, saldo_atual) do
novo_saldo = saldo_atual + valor
IO.puts("🏦 [GenServer] Depósito de R$ #{valor} recebido.")
# {:noreply, novo_saldo} é a versão chique de "loop_do_servidor(novo_saldo)"
{:noreply, novo_saldo}
end
end
Nota de Engenharia: O que é esse @impl true?¶
Você deve ter notado que, antes das funções handle_call e handle_cast, colocamos o atributo @impl true.
Se você vem do Java ou C#, isso é exatamente equivalente ao @Override.
Para que serve?¶
O use GenServer define um Contrato (chamado de Behaviour em Elixir). Ele diz: "Quem usar este módulo DEVE ou PODE implementar funções chamadas init, handle_call, handle_cast, etc."
O @impl true serve dois propósitos:
- Legibilidade (Para Humanos): Ele avisa ao programador que está lendo: "Ei, essa função
handle_castnão foi inventada por mim. Ela faz parte das regras do GenServer." - Segurança (Para o Compilador): Este é o mais importante. Ele age como um corretor ortográfico.
Exemplo do "Salva-Vidas":
Imagine que você digitou errado o nome da função (escreveu handle_cat em vez de handle_cast).
- SEM
@impl: O Elixir vai achar que você criou uma função nova chamadahandle_catpropositalmente. O código compila, mas seu servidor não funciona (as mensagens são ignoradas). Você perde horas debugando. -
COM
@impl: O compilador vai gritar:"Erro: Você marcou handle_cat como uma implementação de GenServer, mas o GenServer não tem nenhuma função com esse nome. Você quis dizer handle_cast?"
Regra de Ouro: Sempre use @impl true nos callbacks (init, handle_call, handle_cast, terminate). É o cinto de segurança do seu código.
Testando a Versão GenServer¶
Volte ao terminal (recompile()). Veja como a experiência de uso (Developer Experience) é superior.
Elixir
# 1. Abrir conta
{:ok, pid} = ContaBancaria.abrir_conta(1000)
# 2. Testar Assincronismo (Cast)
ContaBancaria.depositar(pid, 500)
# :ok (Retorno imediato)
# [GenServer] Depósito de R$ 500 recebido. (Log assíncrono)
# 3. Testar Sincronismo (Call) - Sem precisar de flush()!
ContaBancaria.ver_saldo(pid)
# 1500
# 4. Testar Lógica de Negócio
ContaBancaria.sacar(pid, 2000)
# {:error, "Saldo Insuficiente"}
3. Reflexão de Engenharia¶
Por que migramos do manual para o GenServer?
- Abstração do Loop: Não precisamos escrever a recursão manualmente. O retorno
{:noreply, novo_estado}cuida disso. - Abstração da Comunicação: Não precisamos lidar com PIDs de resposta no
handle_call. O GenServer sabe quem chamou e devolve a resposta. - Tratamento de Erros: O GenServer captura exceções e evita que um crash leve o sistema todo junto (ele isola a falha).
Professor: Agora temos um sistema funcional e limpo. Mas, como engenheiros, devemos ser pessimistas. E se houver um bug no código? E se dividirmos por zero? O processo vai morrer. O dinheiro vai sumir.
Módulo 4: Supervisores e Tolerância a Falhas¶
Objetivo: Transformar nosso processo isolado em uma Árvore de Supervisão (Supervision Tree), criando um sistema que se cura sozinho (Self-healing).
1. Teoria: O Supervisor¶
Um Supervisor é um processo especial. A única função dele é vigiar outros processos (chamados de children ou filhos).
Se um filho morre (crash), o Supervisor percebe o sinal de saída (EXIT) e aplica uma estratégia de reinicialização.
Estratégias Comuns:¶
- :one_for_one (Um por Um): Se o processo A morrer, reinicie apenas o A. (Mais comum).
- :one_for_all (Um por Todos): Se o processo A morrer, mate o B e o C, e reinicie todos. (Usado quando os processos dependem estritamente um do outro).
2. Laboratório Prático: Preparando o Crash¶
Para testar a resiliência, precisamos de uma forma de "quebrar" nossa conta bancária propositalmente.
Passo 1: Adicionando o Botão de Autodestruição¶
Edite o arquivo lib/conta_bancaria.ex. Vamos adicionar uma função que causa um erro fatal (divisão por zero ou raise).
defmodule ContaBancaria do
use GenServer
# ... (Mantenha o código anterior de start_link, init, etc) ...
# Adicione esta função na API do Cliente
def causar_bug(pid) do
GenServer.cast(pid, :bug_fatal)
end
# Adicione este callback lá no final
@impl true
def handle_cast(:bug_fatal, _saldo) do
IO.puts("💣 BOOM! Ocorreu um erro inesperado...")
# raise gera uma exceção que mata o processo na hora
raise "Erro Fatal Simulada pelo Desenvolvedor"
end
# ... (Mantenha os outros handle_cast/handle_call) ...
end
Passo 2: Transformando o Projeto em uma Aplicação¶
Quando criamos o projeto com mix new lab_concorrencia, ele veio "pelado". Precisamos dizer ao Mix que este projeto tem um ponto de entrada (uma Application Callback).
- Crie o arquivo
lib/lab_concorrencia/application.ex:
Elixir
defmodule LabConcorrencia.Application do
# Transforma este módulo em uma Application (ponto de partida do sistema)
use Application
@impl true
def start(_type, _args) do
# Lista de filhos que queremos que iniciem junto com o App
children = [
# Aqui dizemos: "Supervisor, inicie a ContaBancaria com saldo 1000"
{ContaBancaria, 1000}
]
# Opções:
# strategy: :one_for_one -> Se a conta morrer, reinicia ela.
# name: Nome do Supervisor para referência.
opts = [strategy: :one_for_one, name: LabConcorrencia.Supervisor]
IO.puts("🛡️ Supervisor Iniciado. Vigiando processos...")
Supervisor.start_link(children, opts)
end
end
- IMPORTANTE: Precisamos avisar o Mix para carregar esse arquivo.
Abra o arquivo
mix.exsna raiz, procure a funçãoapplicatione altere para:
def application do
[
extra_applications: [:logger],
# Adicione esta linha: 'mod' define o módulo de entrada da aplicação
mod: {LabConcorrencia.Application, []}
]
end
Passo 3: O Teste de Imortalidade (e a Correção do Contrato)¶
Agora que configuramos nosso Supervisor no Application.ex, a teoria diz que ele deve iniciar a conta automaticamente. Vamos testar?
1. O Erro Esperado (A Falha de Contrato)
Reinicie o terminal (iex -S mix).
Ao fazer isso, você provavelmente será recebido por uma mensagem de erro assustadora e o IEx fechará ou mostrará um crash log gigante.
* (Mix) Could not start application lab_concorrencia: ... ** (EXIT)
an exception was raised: ** (UndefinedFunctionError)
function ContaBancaria.start_link/1 is undefined or private
O Diagnóstico de Engenharia:
Por que falhou?
Quando dissemos ao Supervisor para cuidar do filho {ContaBancaria, 1000}, o Supervisor assumiu o comportamento padrão da OTP: ele tentou procurar e executar uma função chamada obrigatoriamente de start_link.
Mas espere! No passo anterior, nós demos o nome criativo de abrir_conta.
- Você: "Quero abrir uma conta."
- Supervisor: "Só sei executar
start_link. Não encontrei, então vou crashar o sistema."
2. A Solução (Obedecendo a Convenção) Vamos renomear nossa função de inicialização para seguir o padrão da indústria.
Edite o arquivo lib/conta_bancaria.ex:
Elixir
defmodule ContaBancaria do
use GenServer
# --- API DO CLIENTE ---
# ANTES: def abrir_conta(saldo_inicial \\ 0) do
# DEPOIS: Renomeamos para start_link para o Supervisor encontrar
def start_link(saldo_inicial \\ 0) do
# Aproveitamos para adicionar name: __MODULE__ (já prepara para o futuro)
GenServer.start_link(__MODULE__, saldo_inicial, name: __MODULE__)
end
# ... mantenha o resto igual ...
end
3. O Teste de Imortalidade (Agora funciona!)
Agora sim. Reinicie o terminal (iex -S mix).
Você deve ver a mensagem: "🛡️ Supervisor Iniciado...".
Agora vamos provar que o sistema se cura sozinho.
A. Encontrando a Vítima: Como o Supervisor iniciou a conta, precisamos descobrir qual PID ele gerou.
# O Supervisor nos diz quem são seus filhos.
[{_, pid_da_conta, _, _}] = Supervisor.which_children(LabConcorrencia.Supervisor)
# Verifique se pegou certo:
IO.inspect(pid_da_conta)
# Ex: #PID<0.155.0>
B. Verificação de Saúde: Garanta que a conta está funcionando:
C. O GRANDE MOMENTO: A Sabotagem:
Vamos invocar a função causar_bug que criamos. Prepare-se para o erro vermelho (que agora é sinal de sucesso).
O que aconteceu?
- O processo morreu (Crash).
- O Supervisor percebeu o sinal de
EXIT. - Imediatamente, ele iniciou um novo processo para substituir o morto.
D. A Prova da Ressurreição: Tente falar com o PID antigo:
Agora, pergunte ao Supervisor quem é o filho dele agora:
Supervisor.which_children(LabConcorrencia.Supervisor)
# Saída: [{ContaBancaria, #PID<0.160.0>, ...}]
Olhe o PID! É um número novo (ex: 160). O Supervisor limpou a bagunça e subiu uma nova instância. Seu banco digital continua no ar, pronto para receber novos clientes.
3. Reflexão de Arquitetura (O "Pulo do Gato")¶
Aluno, observe um detalhe crítico:
Pegue o PID novo e veja o saldo:
[{_, novo_pid, _, _}] = Supervisor.which_children(LabConcorrencia.Supervisor)
ContaBancaria.ver_saldo(novo_pid)
# 1000
Se você tivesse depositado 500 reais antes do crash, o saldo seria 1500. Após o crash, ele voltou para 1000 (estado inicial).
Por quê? Porque processos guardam estado em Memória RAM. Se o processo morre, a memória vai junto. O Supervisor reinicia o processo "do zero" (clean state).
Engenharia de Sistemas Robustos:
- O Supervisor garante disponibilidade: O serviço do banco continua no ar (o processo existe).
- O Banco de Dados garante persistência: Para não perder o saldo, o
GenServerdeveria salvar o valor num banco (Postgres/Redis) a cada depósito e ler do banco noinit.
Parabéns! Você construiu um sistema que:
- Roda concorrentemente (Processos).
- Gerencia estado (GenServer).
- Se recupera de falhas sozinho (Supervisors).
Isso é a base de sistemas como WhatsApp e Discord.
Em um sistema real com milhares de processos, nós não ficamos anotando PIDs (#PID<0.155.0>) num caderninho. PIDs são efêmeros; eles mudam toda vez que o processo morre e renasce. Precisamos de um Nome Fixo (DNS interno). Vamos resolver isso ?
Processos Nomeados (Named Processes)¶
O Problema: Quando o Supervisor reinicia a ContaBancaria após um crash, o PID muda. Se o seu código dependia do PID antigo, ele quebra.
A Solução: Registramos o processo com um nome (geralmente um Átomo). Assim, mandamos mensagens para o Nome, e o Elixir descobre quem é o PID atual daquele nome.
Passo 1: Atualizando o Código¶
Abra lib/conta_bancaria.ex. Vamos fazer apenas duas pequenas alterações:
- No
start_link, vamos adicionar a opçãoname. - Na API do cliente, vamos permitir chamar pelo nome.
Elixir
defmodule ContaBancaria do
use GenServer
# --- API DO CLIENTE ---
# Alteração 1: name: __MODULE__
# Isso registra o processo com o nome "ContaBancaria" (o átomo do módulo).
def start_link(saldo_inicial \\ 0) do
GenServer.start_link(__MODULE__, saldo_inicial, name: __MODULE__)
end
# Alteração 2: Default para o nome
# Se o usuário não passar o PID, assumimos que ele quer falar com a conta padrão.
def ver_saldo(pid_ou_nome \\ __MODULE__) do
GenServer.call(pid_ou_nome, :mostrar_saldo)
end
def depositar(pid_ou_nome \\ __MODULE__, valor) do
GenServer.cast(pid_ou_nome, {:depositar, valor})
end
def sacar(pid_ou_nome \\ __MODULE__, valor) do
GenServer.call(pid_ou_nome, {:sacar, valor})
end
def causar_bug(pid_ou_nome \\ __MODULE__) do
GenServer.cast(pid_ou_nome, :bug_fatal)
end
# ... (O resto do código e callbacks continuam IGUAIS) ...
end
Passo 2: O Teste Definitivo¶
Reinicie seu terminal (iex -S mix) e veja a mágica da Transparência de Localização.
-
Uso direto (Sem PID):Elixir
-
Crash e Recuperação Transparente:Elixir
O que aconteceu?
- O processo antigo morreu.
- O Supervisor criou um novo PID.
- O novo processo, ao nascer, gritou: "Agora EU sou o
ContaBancaria!" - Quando você chamou
ver_saldo(), o Elixir mandou a mensagem para o novo PID automaticamente.
Isso é Alta Disponibilidade. O cliente nem percebeu que o servidor caiu e voltou.
Módulo Intermediário: Especialistas (Agent e Task)¶
Em alguns casos, ao invés de usar GenServer (que faz tudo), podemos usar seus "irmãos menores". Eles são processos especializados:
Agent: Especialista em Guardar Estado (Simples).Task: Especialista em Processamento Paralelo (Cálculo).
1. O Agent (O Cofre de Estado)¶
Imagine que você só quer guardar um valor (como uma configuração global ou um contador) e ler/atualizar isso de forma segura entre vários processos.
Criar um GenServer inteiro com handle_call e handle_cast só para isso é "matar formiga com bazuca".
O Agent abstrai tudo isso.
Prática: O Cofre do Banco¶
Vamos criar um cofre que guarda as reservas totais do banco.
Crie o arquivo lib/cofre.ex:
defmodule Cofre do
# Inicia o Agent.
# A função anônima (fn -> ...) define o estado inicial.
def start_link(valor_inicial) do
Agent.start_link(fn -> valor_inicial end, name: __MODULE__)
end
# Leitura Síncrona
# O Agent.get pega o estado atual e retorna o que você quiser.
# Aqui retornamos o próprio saldo (&(&1) é um atalho para fn x -> x end).
def ver_total do
Agent.get(__MODULE__, fn saldo -> saldo end)
end
# Atualização Síncrona
# O Agent.update altera o estado.
def depositar(valor) do
Agent.update(__MODULE__, fn saldo -> saldo + valor end)
end
end
Teste no Terminal (iex -S mix)¶
Olhe como é simples (sem callbacks complexos):
Elixir
# 1. Iniciar com 50 mil reais
Cofre.start_link(50_000)
# 2. Consultar
Cofre.ver_total()
# 50000
# 3. Atualizar
Cofre.depositar(10_000)
Cofre.ver_total()
# 60000
Conceito de Engenharia: O Agent garante Atomicidade. Se 100 processos tentarem atualizar o cofre ao mesmo tempo, o Agent coloca todos em fila e atende um por um. Não há risco de corromper o dado.
2. A Task (O Trabalhador Assíncrono)¶
E se precisarmos fazer algo demorado (ex: enviar e-mail de confirmação ou gerar PDF) e não quisermos travar o nosso Banco?
Lembre-se: O Elixir é síncrono por padrão. Se você colocar um sleep(5000) no meio do código, tudo para.
A Task serve para jogar esse trabalho para um processo descartável secundário.
Prática: O Notificador¶
Crie o arquivo lib/notificador.ex:
Elixir
defmodule Notificador do
def enviar_email(cliente) do
IO.puts("📧 Iniciando envio de email para #{cliente}...")
# Simula uma conexão lenta com servidor de email (3 segundos)
Process.sleep(5000)
IO.puts("✅ Email enviado com sucesso para #{cliente}!")
end
end
Teste Comparativo (IEx)¶
Cenário A: O jeito travado (Sem Task) Rode isso e veja seu terminal congelar:
Elixir
Notificador.enviar_email("Joao")
# ... (você não consegue digitar nada por 3 segundos) ...
# "✅ Email enviado..."
Em um servidor web, isso seria desastroso. O usuário ficaria vendo a ampulheta girar.
Cenário B: O jeito fluido (Com Task)
Agora vamos usar Task.start/1.
Elixir
Task.start(fn -> Notificador.enviar_email("Maria") end)
# {:ok, #PID<0.165.0>}
# O terminal libera IMEDIATAMENTE! Você pode continuar digitando.
Três segundos depois, a mensagem "✅ Email enviado..." aparece magicamente no meio do que você estiver fazendo.
3. Resumo da Arquitetura: Quando usar o quê?
| Ferramenta | Analogia | Uso Principal | Complexidade |
|---|---|---|---|
| spawn | Célula-tronco | Baixo nível, criar processos brutos (evitar em produção). | Alta (Manual) |
| Task | Freelancer | Tarefas pontuais, paralelas e descartáveis (cálculos, emails). | Baixa |
| Agent | Armário / Cofre | Guardar e compartilhar um estado simples. | Baixa |
| GenServer | Gerente | Lógica de negócio complexa, estado robusto e longa vida. | Média |
Revisão Geral do Modulo¶
Parabéns, caro aluno. Você completou a jornada fundamental da plataforma BEAM. Vamos recapitular o modelo mental que você construiu, pois é isso que importa na sua carreira.
1. O Paradigma Funcional¶
- Imutabilidade: Dados nunca mudam.
lista = [1]cria uma nova lista, não altera a antiga. Isso elimina efeitos colaterais. - Pipeline (
|>): Programar é transformar dados.DadoBruto |> Tratar |> Salvar. - Modules vs Classes: Módulos são sacos de funções. Não guardam estado.
- Pattern Matching: A forma como tomamos decisões (
def handle(10), do: ...) e extraímos dados, substituindoif/elsecomplexos.
2. O Modelo de Concorrência (Atores)¶
- Processos Leves: A unidade básica não é a Thread do SO, mas o Processo da BEAM. Isolados, leves (KB) e massivos (milhões).
- Mensageria: Processos não compartilham memória. Eles trocam cópias de dados via
sendereceive. - Assíncrono (
cast) vs Síncrono (call): A distinção vital em sistemas distribuídos.
3. As Ferramentas de Trabalho¶
Não reinventamos a roda. Usamos abstrações:
Task: Para rodar scripts paralelos ("Faça isso e me avise").Agent: Para guardar estado simples ("Segure este valor").GenServer: O servidor genérico. A peça fundamental para lógica de negócios com estado e concorrência.
4. Tolerância a Falhas (The Erlang Way)¶
- Let it Crash: Não trate erros impossíveis. Deixe o processo morrer.
- Supervisors: O "Anjo da Guarda" que vigia processos e os reinicia para um estado limpo e conhecido.
- Árvore de Supervisão: A arquitetura hierárquica do sistema. Se um worker falha, o supervisor local resolve. Se o supervisor falha, o supervisor dele resolve.
Próximos Passos na sua Carreira¶
Agora que você domina a base, para onde ir?
- Phoenix Framework: É o "Django/Rails" do Elixir, mas capaz de aguentar milhões de conexões WebSocket simultâneas (graças aos Processos que você acabou de aprender!).
- Ecto: A biblioteca de banco de dados. Você aprendeu que processos perdem estado ao reiniciar. O Ecto é quem salva isso no Postgres.
- Broadway/GenStage: Para processamento de dados em massa (Data Pipelines) com back-pressure.
Foi uma honra guiá-lo nesta jornada. Você agora tem o mindset de um Engenheiro de Software Concorrente. Sucesso!