別プロセスを立ち上げ, プロセス間で通信を行う
Elixirではspawn関数を使うことで, 簡単にプロセスを立ち上げ, 何らかの処理を実行させることができます.
試しに以下の様なモジュールを用意して, 別プロセスで実行させてみましょう.
defmodule GreetServer do
def greet do
IO.puts "Hello!"
end
end
iex(6)> spawn GreetServer, :greet, []
spawn GreetServer, :greet, []
Hello!
#PID<0.88.0>
実行結果を見てもよくわからないと思いますが, このIO.putsは別プロセスで実行されているのです.
(なお最後の#PID<0.88.0>はspawn関数の返り値で, 生成されたプロセスのIDです)
このままでは生成されたプロセスが勝手に処理を実行して終わるだけで呼び出し元のプロセスとのメッセージのやりとりは行えていません.
Elixirでは, メッセージを送る方はsend
, 受け取る方はrecieve
を使うことで, プロセス間のメッセージのやり取りを記述します.
上記のサンプルを, メッセージのやり取りを行うように変えたものが下のコードになります.
defmodule GreetServer do
def greet do
receive do
{sender, name} ->
send sender, {:ok, "Hello #{name}"}
end
end
end
defmodule GreetClient do
def run do
pid = spawn GreetServer, :greet, []
greet pid, "Elixir"
end
def greet(pid, name) do
send pid, {self, name}
receive do
{:ok, message} -> IO.puts message
end
end
end
クライアントはサーバを立ち上げた後, "Elixir"という文字列を引数にサーバにメッセージを送り
それを受け取ったクライアントは, 挨拶用の文字列を加工して, 元のメッセージの送信元に送り返しています.
動作確認してみると
iex(3)> GreetClient.run
GreetClient.run
Hello Elixir
:ok
やはりわかりにくいですが, プロセス間のやり取りは成功しているようです.
しかし, クライアントの挙動を少し変えて, 2回メッセージを送るようにしてみると
defmodule GreetClient do
def run do
pid = spawn GreetServer, :greet, []
greet pid, "Elixir"
greet pid, "Erlang"
end
def greet(pid, name) do
send pid, {self, name}
receive do
{:ok, message} -> IO.puts message
end
end
end
iex(5)> GreetClient.run
GreetClient.run
Hello Elixir #<--この出力の後, 処理が停止してしまう
1回目のメッセージ送信に対するレスポンスは取れるものの, 2回目のメッセージに対するレスポンスが取得できずに
処置が停止してしまいます.
これは, receiveはメッセージを一回受け取った時点で待受状態を解除して処理を続行してしまうため
2回目のメッセージは受け取れていないからです.
なので複数回のメッセージを受け取れるようにするには, メッセージ受け取り後の処理が終わった後に再度待ち受け状態にならないといけないわけです.
ちなみに, C言語などではこういう場合は無限ループで処理を書いたりするものですが
Elixirでは再帰で書くのが一般的のようです.(当然末尾再帰になってないと大変なことになるのでしょうが)
defmodule GreetServer do
def greet do
receive do
{sender, message} ->
send sender, {:ok, "Hello #{message}"}
greet #<-- 自分自身を呼び出し, 再び待ち受け状態になる
end
end
end
あらためて動作を確認してみると
iex(3)> GreetClient.run
GreetClient.run
Hello Elixir
Hello Erlang
:ok
今度は停止することなく処理が最後まで実行されした.
一つのプロセスに対して, 複数のプロセスが同時にメッセージを送った場合の挙動
上の例で, クライアントとサーバが1:1の場合の挙動を確認できましたが, n:1の時の挙動は確認できなかったので,
以下の様なサンプルコードを用意して実行してみました.
defmodule GreetServer2 do
import :timer, only: [sleep: 1]
def greet do
receive do
{sender, name} ->
IO.puts "Before sleep for #{name}"
sleep 1000
IO.puts "After sleep for #{name}"
send sender, {:ok, "Hello #{name}"}
greet
end
end
end
defmodule GreetClient2 do
def run do
server = spawn GreetServer2, :greet, []
client1 = spawn GreetClient2, :greet, [self]
client2 = spawn GreetClient2, :greet, [self]
send client1, {server, "Elixir"}
send client2, {server, "Erlang"}
wait_all [client1, client2]
end
def greet(parent) do
receive do
{server, name} -> send server, {self, name}
receive do
{:ok, message} -> IO.puts message
send parent, {self, :finished}
end
end
end
def wait_all([]) do
:ok
end
def wait_all(clients) do
receive do
{client, :finished} ->
do_wait List.delete(clients, client)
end
end
end
プログラムは4つのプロセスからできております.
メインプロセスではサーバプロセス1つとクライアントプロセス2つを立ち上げ, クライアントプロセスのタスク終了を待つ.
クライアントプロセスは, 指定されたサーバにメッセージを送り, 帰ってきた結果を標準出力に出力後, 親に当たるメインプロセスにタスク完了のメッセージを送る.
サーバプロセスは, これまでのものと大して変わりませんが, 挙動を確認するために1000msのスリープを入れています.
これを実行した結果は以下のとおり.
iex(3)> GreetClient2.run
GreetClient2.run
Before sleep for Elixir #<-- client1からのmessageを処理開始
After sleep for Elixir
Before sleep for Erlang #<-- client2からのmessageを処理開始
Hello Elixir
After sleep for Erlang
Hello Erlang
:ok
見てわかるように, スリープ中に届いたであろう2個めのメッセージが消えることもなく
またそれぞれのメッセージに対する処理が入れ子になるようなこともなく順番の処理されていることがわかります.
プロセス間でメッセージをやり取りする際に, 直接送り合うわけではなく, 中間にメッセージをストアする為の仕組みを用意して, そこに投げ込んだり, 自分宛てのメッセージを探したりするとよい
という話を大昔に何かの本で読んだ気がするのでそんな感じの実装になっているのでしょうか?
まあ, とにかく, 他のプロセスにメッセージを送る時は相手側の状態とかは特に気にすることなく
送ってしまってよいようです.