Do you want to pick up from where you left of?
Take me there

Concorrência

Um dos pontos ofertados pelo Elixir é o suporte a concorrência. Graças à Erlang VM (BEAM), concorrência no Elixir é mais fácil do que esperamos. O modelo de concorrência depende de Atores, um processo contido (isolado) que se comunica com outros processos por meio de passagem de mensagem.

Nesta aula nós veremos os módulos de concorrência que vêm com Elixir. No próximo capítulo, cobriremos os comportamentos OTP que os implementam

Processos

Processos no Erlang VM são leves e executam em todas as CPUs. Enquanto eles podem parecer como threads nativas, eles são bastantes simples e não é incomum ter milhares de processos concorrentes em uma aplicação Elixir.

A forma mais fácil para criar um novo processo é o spawn, que pode receber tanto uma função nomeada quanto anônima. Quando criamos um novo processo ele retorna um Process Identifier ou PID, para exclusivamente identificá-lo dentro de nossa aplicação.

Para iniciar criaremos um módulo e definiremos uma função que gostaríamos de executar:

defmodule Example do
  def add(a, b) do
    IO.puts(a + b)
  end
end

iex> Example.add(2, 3)
5
:ok

Para executar a função de forma assíncrona usamos spawn/3:

iex> spawn(Example, :add, [2, 3])
5
#PID<0.80.0>

Passagem de mensagem

Para comunicar-se, os processos dependem de passagem de mensagens. Há dois componentes principais para isso: send/2 e receive. A função send/2 nos permite enviar mensagens para PIDs. Para recebê-las, usamos a função receive com pattern matching para selecionar as mensagens. Se nenhum padrão coincidir com a mensagem recebida, a execução continua ininterrupta.

defmodule Example do
  def listen do
    receive do
      {:ok, "hello"} -> IO.puts("World")
    end

    listen()
  end
end

iex> pid = spawn(Example, :listen, [])
#PID<0.108.0>

iex> send pid, {:ok, "hello"}
World
{:ok, "hello"}

iex> send pid, :ok
:ok

Você pode notar que a função listen/0 é recursiva, isso permite que nosso processo receba múltiplas mensagens. Sem recursão nosso processo teria saído depois de receber a primeira mensagem.

Vinculando Processos

Um problema com spawn é saber quando um processo falha. Para isso, precisamos vincular nossos processos usando spawn_link. Dois processos vinculados receberão notificações de saída um do outro:

defmodule Example do
  def explode, do: exit(:kaboom)
end

iex> spawn(Example, :explode, [])
#PID<0.66.0>

iex> spawn_link(Example, :explode, [])
** (EXIT from #PID<0.57.0>) evaluator process exited with reason: :kaboom

Em determinados momentos não queremos que nosso processo vinculado falhe o atual. Para isso nós precisamos interceptar as saídas usando Process.flag/2. Ela usa a função do erlang process_flag/2 para a flag trap_exit. Quando interceptando saídas (trap_exit é definida como true), sinais de saída são recebidos como uma tupla de mensagem: {:EXIT, from_pid, reason}.

defmodule Example do
  def explode, do: exit(:kaboom)

  def run do
    Process.flag(:trap_exit, true)
    spawn_link(Example, :explode, [])

    receive do
      {:EXIT, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
    end
  end
end

iex> Example.run
Exit reason: kaboom
:ok

Monitorando processos

E se não queremos vincular dois processos, mas continuar a sermos informados? Para isso, podemos usar o monitoramento de processos com spawn_monitor. Quando monitoramos um processo, nós recebemos uma mensagem informando se o processo falhou, sem afetar nosso processo atual nem necessitar explicitamente interceptar a saída.

defmodule Example do
  def explode, do: exit(:kaboom)

  def run do
    {pid, ref} = spawn_monitor(Example, :explode, [])

    receive do
      {:DOWN, _ref, :process, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
    end
  end
end

iex> Example.run
Exit reason: kaboom
:ok

Agentes

Agentes são uma abstração acerca de processos em segundo plano que mantêm estado. Podemos acessá-los de outros processos dentro de nossa aplicação ou nó. O estado do nosso Agente é definido como valor de retorno de nossa função:

iex> {:ok, agent} = Agent.start_link(fn -> [1, 2, 3] end)
{:ok, #PID<0.65.0>}

iex> Agent.update(agent, fn (state) -> state ++ [4, 5] end)
:ok

iex> Agent.get(agent, &(&1))
[1, 2, 3, 4, 5]

Quando nomeamos um Agente podemos referenciar seu nome ao invés de seu PID:

iex> Agent.start_link(fn -> [1, 2, 3] end, name: Numbers)
{:ok, #PID<0.74.0>}

iex> Agent.get(Numbers, &(&1))
[1, 2, 3]

Tarefas

Tarefas fornecem uma forma para executar uma função em segundo plano e posteriormente recuperar seu valor. Elas podem ser particularmente úteis ao manusear operações custosas, sem bloquear a execução do aplicativo.

defmodule Example do
  def double(x) do
    :timer.sleep(2000)
    x * 2
  end
end

iex> task = Task.async(Example, :double, [2000])
%Task{
  owner: #PID<0.105.0>,
  pid: #PID<0.114.0>,
  ref: #Reference<0.2418076177.4129030147.64217>
}

# Realizar algum trabalho

iex> Task.await(task)
4000
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!