Objectif
Nous allons voir les différentes façons qu’ont les acteurs pour découvrir d’autres acteurs présents sur le même nœud.
Parent<>Enfant
On ne peut pas vraiment parler de “découverte” ici mais je le mentionne quand même car c’est le cas que nous avons rencontré jusqu’à présent. Par exemple dans cet article : Un acteur transitoire qui produit un résultat. Quand on utilise la fonction spawn (peu importe l’arité), celle-ci retourne directement le pid.
child_pid =
spawn(fn ->
receive do
msg -> IO.puts("child received: #{msg}")
end
end)
send(child_pid, "hello")
child received: hello
"hello"
Info : Le “hello” ici est le retour de la fonction
send/2. Elle retourne son 2ème paramètre.
Il est normal que le parent connaisse l’enfant mais l’inverse n’est pas vrai. Pour que l’enfant connaisse son géniteur, il suffit que celui-ci lui donne son pid via un message ou via la closure (contexte d’une fonction anonyme).
# avec une closure
parent_pid = self()
child_pid =
spawn(fn ->
receive do
{:feed, food} -> send(parent_pid, "I hate #{food}!")
end
end)
send(child_pid, {:feed, "green beans"})
iex(8)> flush()
"I hate green beans!"
:ok
# avec un message
child_pid =
spawn(fn ->
receive do
{:feed, food, parent_pid} -> send(parent_pid, "I hate #{food}!")
end
end)
send(child_pid, {:feed, "green beans", self()})
iex(8)> flush()
"I hate green beans!"
Info :
flush/0est une fonction disponible uniquement dans le terminal qui permet de vider et d’afficher les messages qui se trouvent dans lamailboxde l’acteur.
Named processes
Pour les acteurs permanents uniques, on peut tout simplement nommer les acteurs avec la fonction Process.register/2. Le nom doit être un atom. Un acteur peut avoir plusieurs noms. Un nom n’est par contre lié qu’à un seul acteur.
iex(4)> Process.whereis(:mon_shell)
nil
iex(5)> Process.register(self(), :mon_shell)
true
iex(6)> Process.whereis(:mon_shell)
#PID<0.105.0>
iex(7)> send(:mon_shell, :un_message)
:un_message
iex(8)> flush
:un_message
:ok
J’ai utilisé Process.whereis/1 qui retourne un pid associé à un nom pour la démo, mais, comme on peut le voir, on n’en a même pas besoin car la fonction send/2 fait déjà ce travail d’association.
En pratique on utilisera souvent la macro __MODULE__ pour nommer un acteur permanent. Cette macro retourne le nom du module.
Registres
Pour découvrir des acteurs dynamiques, il existe le module Registry. Ce module permet de créer un acteur qui va s’occuper de faire la correspondance entre une clé et un ou plusieurs pids. Les différences par rapport aux named processes sont les suivantes :
- il peut y avoir plusieurs acteurs derrière la même clé (sauf si le flag keys: :unique est donné)
- le nom peut être n’importe quoi (un string, un tuple etc.)
Reprenons l’exemple du compteur de cet article : Un acteur permanent qui préserve un état et imaginons qu’on a besoin d’un compteur pour compter le nombre de vues de pages.
defmodule Counter do
use Agent
# j'ai ajouté un argument optionnel
# pour pouvoir passer un nom en paramètre
# C'est l'équivalent de Counter.start/0 et Process.register/2 en suivant
def start(opts \\ []) do
Agent.start(fn -> 0 end, opts)
end
def stop(pid) do
Agent.stop(pid)
end
def count(pid) do
{:ok, Agent.get(pid, fn count -> count end)}
end
def incr(pid) do
Agent.update(pid, fn count -> count + 1 end)
count(pid)
end
end
iex(2)> {:ok, _} = Registry.start_link(keys: :unique, name: PageCounters)
{:ok, #PID<0.111.0>}
iex(3)> name = {:via, Registry, {PageCounters, "My socks collection"}}
{:via, Registry, {PageCounters, "My socks collection"}}
iex(4)> {:ok, _} = Counter.start(name: name)
{:ok, #PID<0.113.0>}
iex(5)> Counter.count(name)
{:ok, 0}
iex(6)> Counter.incr(name)
{:ok, 1}
Convention : Le tuple {:via, _, _} n’est pas sorti de mon chapeau. C’est une convention qui permet de résoudre directement les
pidsans avoir à faire de Registry.lookup/2
N’importe quel autre acteur peut accéder à ce compteur à partir du moment où il connaît le nom du registre : PageCounters et le nom de la page : "My socks collection". Il est aussi possible de lister les acteurs d’un même registre via Registry.select/2. Mais attention, la syntaxe fait saigner des yeux :
iex(8)> Registry.select(PageCounters, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}])
[
{"I found a way to solve P=NP", #PID<0.114.0>, nil},
{"My socks collection", #PID<0.113.0>, nil}
]
Conclusion
Grâce à l’utilisation de ces deux techniques, on devrait moins voir de pid se balader pour nos acteurs permanents. Les deux techniques sont complètement intégrées dans la librairie standard et toutes les fonctions d’envoi de messages (il y en a un paquet) acceptent donc des pids, des names, ou des :via tuple.
Dans le prochain article on va commencer à aborder la distribution. Il serait dommage de se restreindre à un seul nœud n’est-ce pas ?