Elixirでは、すべてのコードはプロセスの中で実行されます。Elixirのプロセスには、次のような特徴があります。
_何百_ものプロセスを動かすElixirアプリは珍しくありませんが、特にそれがライブラリであれば、新しいプロセスを明示的に一切作らないアプリも普通にあります。
デフォルトでは、関数はそれを呼び出したのと同じプロセスの中で実行されます。ある関数を新しいプロセスで明示的に実行したいときは、spawnを使います。
spawn/1は、そのまま実行する関数を引数に取ります。
spawn(fn -> 2 + 2 end)
# => #PID<0.125.0>
spawn/3は、実行する関数を、モジュール名、関数名(アトム)、その関数に渡す引数のリストで指定します。
spawn(String, :split, ["hello there", " "])
# => #PID<0.113.0>
プロセスは、その関数の実行が終わるとすぐに終了します。
プロセスがまだ_生きている_(実行中である)かどうかは、Process.alive?/1で確認できます。
pid = spawn(fn -> 2 + 2 end)
Process.alive?(pid)
# => false
プロセス同士が直接情報を共有することはありません。プロセスはデータを共有するために_メッセージを送り_ます。この並行処理のパターンは、アクターモデルと呼ばれます。
プロセスにメッセージを送るには、send/2を使います。
send(pid, :hello)
sendは、メッセージが受信されたかどうかも、受信側がまだ生きているかどうかも確認しません。メッセージはどのような型でもかまいません。
現在のプロセスに送られたメッセージは、receive/1で受け取ることができます。
receiveは、指定したいずれかのパターンに一致するメッセージがプロセスのメールボックスに入るまで待ちます。
afterブロックを使ってタイムアウトを指定できます。receive/1には必ずすべてに一致する_節を書いてください。receive do
{:ping, sender_pid} -> send(sender_pid, :pong)
_ -> nil
after
5000 ->
{:error, "No message in 5 seconds"}
end
複数のメッセージを受け取りたいときは、receive/1を再帰的に呼び出す必要があります。receive/1を呼び出し、メッセージに対して何らかの処理を行い、さらにメッセージを待つために自分自身を呼び出す再帰関数(たとえばloopという名前の関数)を実装するのは、よくあるパターンです。あるreceive/1の呼び出しから次の呼び出しへ状態を持ち越したいときは、そのloop関数に引数を渡すことで実現できます。
def loop(state) do
receive do
:increment_by_one ->
loop(state + 1)
{:report_state, sender_pid} ->
send(sender_pid, state)
loop(state)
:stop ->
nil
_ ->
loop(state)
end
end
実際には、この方法を直接使うことはほとんどありません。Elixirには、AgentモジュールやGenServerビヘイビアなど、どちらも受信ループの上に成り立つ並行処理の抽象化が用意されています。ただし、これらの抽象化を効率よく使いこなすには、基本をしっかり理解しておくことが欠かせません。