プロセス

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 になっている(:okflush 自体の返り値)。
今度は、値を 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