Les processus Elixir sont isolés et ne partagent rien par défaut. Quand un processus enfant non lié plante, son processus parent n'est pas affecté.
On peut modifier ce comportement en liant les processus entre eux. Si deux processus sont liés, une défaillance dans l'un est propagée à l'autre. Les liens sont bidirectionnels.
On peut lancer des processus déjà liés au processus appelant avec spawn_link/1, une opération atomique, ou les lier plus tard avec Process.link/1.
Lier des processus peut être utile pour du travail parallélisé, quand une partie du travail ne doit pas continuer si une autre partie n'arrive pas à se terminer.
On peut aussi utiliser la liaison pour superviser des processus. Si un processus intercepte les sorties, il ne plantera pas quand un processus auquel il est lié plante. Il recevra à la place un message à propos du plantage. Cela lui permet de gérer le plantage proprement, par exemple en redémarrant le processus qui a planté.
On peut configurer un processus pour qu'il intercepte les sorties en appelant Process.flag(:trap_exit, true). Note que Process.flag/2 renvoie l'ancienne valeur du drapeau, pas la nouvelle.
Le message envoyé au processus si un processus lié plante correspondra au motif {:EXIT, from, reason}, où from est un PID. Si reason est autre chose que l'atome :normal, cela signifie que le processus a planté ou a été tué de force.
Les tâches sont des processus destinés à exécuter une opération précise. Elles ne communiquent généralement pas avec les autres processus, mais elles peuvent renvoyer un résultat au processus qui a démarré la tâche.
On utilise couramment les tâches pour paralléliser le travail.
async/await
Pour démarrer une tâche, utilise Task.async/1. Elle prend une fonction anonyme en argument et l'exécute dans un nouveau processus lié au processus appelant. Elle renvoie une structure %Task{}.
Pour récupérer le résultat de l'exécution, passe la structure %Task{} à Task.await/2. Elle attendra que la tâche se termine et renverra son résultat. Le deuxième argument est un délai d'attente en millisecondes, dont la valeur par défaut est 5000.
Note qu'entre le démarrage de la tâche et l'attente de son résultat, le processus qui a démarré la tâche n'est pas bloqué et peut effectuer d'autres opérations.
Toute tâche démarrée avec Task.async/1 doit être attendue, car elle enverra un message au processus appelant. On ne peut appeler Task.await/2 qu'une seule fois par tâche.
start/start_link
Si tu veux démarrer une tâche uniquement pour ses effets de bord, utilise Task.start/1 ou Task.start_link/1. Task.start/1 démarre une tâche qui n'est pas liée au processus appelant, et Task.start_link/1 démarre une tâche qui lui est liée. Les deux fonctions renvoient un tuple {:ok, pid}.
Tu poursuis ton travail chez Instruments of Texas sur une calculatrice RPN expérimentale. Ton équipe a construit quelques prototypes qui doivent faire l'objet d'une inspection approfondie, afin de choisir le meilleur, celui qui pourra être fabriqué en série.
Tu veux mener deux types de vérifications.
D'abord, une vérification de fiabilité qui détectera les entrées pour lesquelles la calculatrice inspectée plante ou ne répond pas assez vite. Pour isoler les défaillances, les calculs de chaque entrée doivent être exécutés dans un processus séparé. Le fait de lier le processus et de piéger les sorties dans le processus appelant permet de détecter si le calcul s'est terminé ou s'il a planté.
Ensuite, une vérification de justesse qui contrôlera, pour une entrée donnée, si le résultat renvoyé par la calculatrice est celui attendu. Seules les calculatrices qui ont déjà réussi la vérification de fiabilité passeront la vérification de justesse, donc les plantages ne posent pas de problème. Cependant, les opérations doivent être exécutées de façon concurrente pour accélérer le processus, ce qui en fait un cas d'usage idéal pour les tâches asynchrones.
Implémente la fonction RPNCalculatorInspection.start_reliability_check/2. Elle prend 2 arguments, une fonction (la calculatrice) et une entrée pour la calculatrice. Elle doit renvoyer une map qui contient l'entrée et le PID du processus créé.
Le processus créé doit appeler la fonction calculatrice donnée avec l'entrée donnée. Le processus doit être lié au processus appelant.
RPNCalculatorInspection.start_reliability_check(fn _ -> 0 end, "2 3 +")
# => %{input: "2 3 +", pid: #PID<0.169.0>}
Implémente la fonction RPNCalculatorInspection.await_reliability_check_result/2. Elle prend deux arguments. Le premier est une map contenant l'entrée de la vérification de fiabilité et le PID du processus qui exécute la vérification de fiabilité pour cette entrée, telle que renvoyée par RPNCalculatorInspection.start_reliability_check/2. Le second est une map qui sert d'accumulateur pour les résultats des vérifications de fiabilité portant sur différentes entrées.
La fonction doit attendre un message de sortie.
Si elle reçoit un message de sortie ({:EXIT, from, reason}) avec la raison :normal, provenant du même processus que celui qui exécute la vérification de fiabilité, elle doit renvoyer la map de résultats en ajoutant la valeur :ok sous la clé input.
Si elle reçoit un message de sortie avec une raison différente, provenant du même processus que celui qui exécute la vérification de fiabilité, elle doit renvoyer la map de résultats en ajoutant la valeur :error sous la clé input.
Si elle ne reçoit aucun message correspondant à ces critères dans les 100 ms, elle doit renvoyer la map de résultats en ajoutant la valeur :timeout sous la clé 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}
Implémente la fonction RPNCalculatorInspection.reliability_check/2. Elle prend 2 arguments, une fonction (la calculatrice) et un tableau d'entrées pour la calculatrice.
Pour chaque entrée du tableau, elle doit démarrer la vérification de fiabilité dans un nouveau processus lié en utilisant start_reliability_check/2. Ensuite, pour chaque processus ainsi démarré, elle doit attendre ses résultats en utilisant await_reliability_check_result/2.
Avant de démarrer le moindre processus, la fonction doit activer sur le processus courant l'option qui permet de piéger les sorties, afin de pouvoir recevoir les messages de sortie. Ensuite, elle doit rétablir cette option à sa valeur d'origine.
La fonction doit renvoyer une map contenant les résultats des vérifications de fiabilité de toutes les entrées.
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
# }
Implémente la fonction RPNCalculatorInspection.correctness_check/2. Elle prend 2 arguments, une fonction (la calculatrice) et un tableau d'entrées pour la calculatrice.
Pour chaque entrée du tableau, elle doit démarrer une tâche asynchrone qui appellera la calculatrice avec l'entrée donnée. Ensuite, pour chaque tâche ainsi démarrée, elle doit attendre ses résultats pendant 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]
Inscris-toi sur Exercism pour apprendre et maîtriser Elixir avec 58 concepts168 exercices, et un vrai mentorat humain, le tout gratuitement.