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