cf. 11 プロセス – Processes – Elixir
Elixir ではすべてのコードはプロセスの内部で動作する。このプロセスは OS のプロセスとは別物。非常に軽量で、同時に何千ものプロセスを動かすことも少なくないという。
各々のプロセスは独立、並行して動き、メッセージパッシングでやり取りする。
spawn(生み出す)
新しいプロセスを生み出すには spawn/1
を使う。
iex(1)> spawn fn -> 1 + 2 end
#PID<0.82.0>
spawn/1
は関数を引数にとり、新しいプロセスの中で実行する。関数が終わるとプロセスも死ぬ。だから上の例では新しいプロセスはすぐ死んでいるはずだ。spawn/1
は PID(プロセス識別子)を返すので、Process.alive?/1
でプロセスが生きているかどうかを確かめるてみよう。
iex(2)> pid = spawn fn -> 1 + 2 end
#PID<0.84.0>
iex(3)> Process.alive?(pid)
false
self/0
は自身の PID を返す。
iex(4)> self()
#PID<0.80.0>
iex(5)> Process.alive?(self())
true
メッセージの送信と受信
send/2
でメッセージを送り、receive/1
で受け取る。
iex(6)> send self(), {:hello, "world"}
{:hello, "world"}
iex(7)> receive do
...(7)> {:hello, msg} -> msg
...(7)> {:world, msg} -> "won't match"
...(7)> end
"world"
送られてきたメッセージはメールボックスの中にたまり、receive/1
はその中からマッチするメッセージを受け取る。
もしメールボックスにマッチするメッセージがなければ、マッチするメッセージがやってくるまで待ち続ける。とはいえ、待ち時間を指定することもできる。
iex(8)> receive do
...(8)> {:hello, msg} -> msg
...(8)> after
...(8)> 1000 -> "nothing after 1s"
...(8)> end
"nothing after 1s"
上の例では、1000 ミリ秒待った後、メッセージがないという出力をしている。
つぎは、プロセス同士でメッセージを送る例。
iex(9)> parent = self()
#PID<0.80.0>
iex(10)> spawn fn -> send(parent, {:hello, self()}) end
#PID<0.100.0>
iex(11)> receive do
...(11)> {:hello, pid} -> "Got hello from #{inspect(pid)}"
...(11)> end
"Got hello from #PID<0.100.0>"
spawn/1
で作られたプロセスから親のプロセスにメッセージを送って、親プロセスで受け取っている。
プロセスの失敗
子プロセスでエラーが起きても、親プロセスに被害は及ばない。単にエラーメッセージが表示されるだけだ。
iex(12)> spawn fn -> raise "oops" end
#PID<0.105.0>
iex(13)>
14:24:13.188 [error] Process #PID<0.105.0> raised an exception
** (RuntimeError) oops
:erlang.apply/2
リンク
リンクを使うと事情が異なる。子プロセスで起きたエラーが親プロセスに影響を与える。spawn_link/1
を使う。
iex(13)> spawn_link fn -> raise "oops" end
14:27:12.461 [error] Process #PID<0.107.0> raised an exception
** (RuntimeError) oops
:erlang.apply/2
** (EXIT from #PID<0.80.0>) an exception was raised:
** (RuntimeError) oops
:erlang.apply/2
Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
よく見ると iex のプロンプトの番号が 1 に戻っている。これは親プロセスも一度死んで、再起動されたってことなんだろうか。
状態
Elixir のプロセスは、状態を保存しておくのにもつかわれる。
defmodule KV do
def start_link do
{:ok, spawn_link(fn -> loop(%{}) end)}
end
defp loop(map) do
receive do
{:get, key, caller} ->
send caller, Map.get(map, key)
loop(map)
{:put, key, value} ->
loop(Map.put(map, key, value))
end
end
end
上のモジュール KV
を iex 上で使ってみよう。flush
はメールボックスのメッセージをすべて表示してからにする関数。
^o^ > iex kv.exs
Eshell V8.0 (abort with ^G)
Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, pid} = KV.start_link
{:ok, #PID<0.86.0>}
iex(2)> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.84.0>}
iex(3)> flush
nil
:ok
KV.start_link
した直後に、値をとろうとしても何もないので、flush
の結果は nil
になっている(:ok
は flush
自体の返り値)。
今度は、値を put メッセージで送った後に get してみよう。
iex(4)> send pid, {:put, :hello, "world"}
{:put, :hello, "world"}
iex(5)> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.84.0>}
iex(6)> flush
"world"
:ok
こんどは “world” が返ってきている。このように、プロセスに値(状態)を保存することができる。
最後に、Elixir の提供する agent を紹介しておく。agent は上のような KV
モジュールを書かなくとも、状態を簡単に保存するもの、と考えれば、とりあえずよさそうだ。
iex(7)> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.93.0>}
iex(8)> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
:ok
iex(9)> Agent.get(pid, fn map -> Map.get(map, :hello) end)
:world