Os processos de Elixir são isolados e, por predefinição, não partilham nada. Quando um processo filho que não está ligado falha, o processo pai não é afetado.
Este comportamento pode ser alterado ligando processos uns aos outros. Se dois processos estiverem ligados, uma falha num processo será propagada para o outro processo. As ligações são bidirecionais.
Os processos podem ser criados já ligados ao processo que os invoca com spawn_link/1, que é uma operação atómica, ou podem ser ligados mais tarde com Process.link/1.
Ligar processos pode ser útil em trabalho paralelizado, quando cada pedaço de trabalho não deve continuar se outro pedaço não conseguir terminar.
Ligar também pode ser usado para supervisionar processos. Se um processo capturar saídas, não falha quando um processo ao qual está ligado falha. Em vez disso, recebe uma mensagem sobre a falha. Isto permite-lhe lidar com a falha de forma controlada, por exemplo reiniciando o processo que falhou.
Um processo pode ser configurado para capturar saídas chamando Process.flag(:trap_exit, true). Repara que Process.flag/2 devolve o valor antigo do sinalizador, não o novo.
A mensagem que será enviada ao processo caso um processo ligado falhe segue o padrão {:EXIT, from, reason}, em que from é um PID. Se reason for algo diferente do átomo :normal, isso significa que o processo falhou ou foi terminado à força.
As tarefas são processos destinados a executar uma operação específica. Normalmente não comunicam com outros processos, mas podem devolver um resultado ao processo que iniciou a tarefa.
As tarefas são frequentemente usadas para paralelizar trabalho.
async/await
Para iniciar uma tarefa, usa Task.async/1. Recebe uma função anónima como argumento e executa-a num novo processo ligado ao processo que a invoca. Devolve uma estrutura %Task{}.
Para obter o resultado da execução, passa a estrutura %Task{} a Task.await/2. Vai esperar que a tarefa termine e devolver o seu resultado. O segundo argumento é um tempo limite em milissegundos, com o valor predefinido de 5000.
Repara que, entre iniciar e aguardar a tarefa, o processo que a iniciou não fica bloqueado e pode realizar outras operações.
Qualquer tarefa iniciada com Task.async/1 deve ser aguardada, porque envia uma mensagem ao processo que a invoca. Só se pode chamar Task.await/2 uma vez por tarefa.
start/start_link
Se quiseres iniciar uma tarefa apenas para efeitos secundários, usa Task.start/1 ou Task.start_link/1. Task.start/1 inicia uma tarefa que não está ligada ao processo que a invoca, e Task.start_link/1 inicia uma tarefa ligada ao processo que a invoca. Ambas as funções devolvem um tuplo {:ok, pid}.
O teu trabalho na Instruments of Texas numa calculadora RPN experimental continua. A tua equipa construiu alguns protótipos que precisam de passar por uma inspeção minuciosa, para escolher o melhor, o que possa ser produzido em série.
Queres fazer dois tipos de verificações.
Em primeiro lugar, uma verificação de fiabilidade, que deteta os valores de entrada para os quais a calculadora em inspeção falha ou não responde com a rapidez suficiente. Para isolar as falhas, os cálculos de cada valor de entrada têm de ser executados num processo separado. A ligação de processos e a captura de saídas no processo que invoca podem ser usadas para detetar se o cálculo terminou ou falhou.
Em segundo lugar, uma verificação de correção, que verifica se, para um determinado valor de entrada, o resultado devolvido pela calculadora é o esperado. Só as calculadoras que já passaram na verificação de fiabilidade é que são submetidas à verificação de correção, por isso as falhas não são um problema. No entanto, as operações devem ser executadas em simultâneo para acelerar o processo, o que faz disto o caso de uso perfeito para tarefas assíncronas.
Implementa a função RPNCalculatorInspection.start_reliability_check/2. Deve receber 2 argumentos: uma função (a calculadora) e um valor de entrada para a calculadora. Deve devolver um mapa que contém o valor de entrada e o PID do processo criado.
O processo criado deve chamar a função calculadora recebida com o valor de entrada recebido. O processo deve ficar ligado ao processo que o invoca.
RPNCalculatorInspection.start_reliability_check(fn _ -> 0 end, "2 3 +")
# => %{input: "2 3 +", pid: #PID<0.169.0>}
Implementa a função RPNCalculatorInspection.await_reliability_check_result/2. Deve receber dois argumentos. O primeiro argumento é um mapa com o valor de entrada da verificação de fiabilidade e o PID do processo que está a executar a verificação de fiabilidade para esse valor de entrada, tal como devolvido por RPNCalculatorInspection.start_reliability_check/2. O segundo argumento é um mapa que serve de acumulador para os resultados de verificações de fiabilidade com valores de entrada diferentes.
A função deve aguardar uma mensagem de saída.
Se receber uma mensagem de saída ({:EXIT, from, reason}) com o motivo :normal do mesmo processo que executa a verificação de fiabilidade, deve devolver o mapa de resultados com o valor :ok adicionado sob a chave input.
Se receber uma mensagem de saída com um motivo diferente do mesmo processo que executa a verificação de fiabilidade, deve devolver o mapa de resultados com o valor :error adicionado sob a chave input.
Se não receber nenhuma mensagem que cumpra estes critérios em 100 ms, deve devolver o mapa de resultados com o valor :timeout adicionado sob a chave input.
# when an exit message is waiting for the process in its inbox
send(self(), {:EXIT, pid, :normal})
RPNCalculatorInspection.await_reliability_check_result(
%{input: "5 7 -", pid: pid},
%{}
)
# => %{"5 7 -" => :ok}
# when there are no messages in the process inbox
RPNCalculatorInspection.await_reliability_check_result(
%{input: "3 2 *", pid: pid},
%{"5 7 -" => :ok}
)
# => %{"5 7 -" => :ok, "3 2 *" => :timeout}
Implementa a função RPNCalculatorInspection.reliability_check/2. Deve receber 2 argumentos: uma função (a calculadora) e uma lista de valores de entrada para a calculadora.
Para cada valor de entrada da lista, deve iniciar a verificação de fiabilidade num novo processo ligado, utilizando start_reliability_check/2. Depois, para cada processo iniciado desta forma, deve aguardar os seus resultados, utilizando await_reliability_check_result/2.
Antes de iniciar quaisquer processos, a função tem de marcar o processo atual para capturar saídas, de forma a conseguir receber mensagens de saída. Depois disso, deve repor esta marca no valor original.
A função deve devolver um mapa com os resultados das verificações de fiabilidade de todos os valores de entrada.
fake_broken_calculator = fn input ->
if String.ends_with?(input, "*"), do: raise("oops")
end
inputs = ["2 3 +", "10 3 *", "20 2 /"]
RPNCalculatorInspection.reliability_check(fake_broken_calculator, inputs)
# => %{
# "2 3 +" => :ok,
# "10 3 *" => :error,
# "20 2 /" => :ok
# }
Implementa a função RPNCalculatorInspection.correctness_check/2. Deve receber 2 argumentos: uma função (a calculadora) e uma lista de valores de entrada para a calculadora.
Para cada valor de entrada da lista, deve iniciar uma tarefa assíncrona que vai chamar a calculadora com o valor de entrada recebido. Depois, para cada tarefa iniciada desta forma, deve aguardar os seus resultados durante 100 ms.
fast_cheating_calculator = fn input -> 14 end
inputs = ["13 1 +", "50 2 *", "1000 2 /"]
RPNCalculatorInspection.correctness_check(fast_cheating_calculator, inputs)
# => [14, 14, 14]
Inscreve-te no Exercism para aprenderes e dominares Elixir com 58 conceitos168 exercícios, e mentoria humana real, tudo grátis.