URL
試した環境
- Ubuntu Server 14.04 LTS
- Erlang/OTP 18
- Elixir 1.0.4
Processes
Elixirでは、全てのコードはプロセス内部で実行される。
プロセス同士はお互いに独立し、並行して実行され、メッセージパッシングを通してコミュニケーションする。プロセスは並行の基礎となるだけではなく、分散や耐障害性のあるプログラミングを構築する基礎も提供する。
ElixirのプロセスをOSのプロセスと混同してはいけない。Elixirのプロセスは(多くのプログラミング言語のスレッドとは異なり)メモリとCPUにとって非常に軽量である。そのため、同時に数万のプロセスが実行されることは珍しくない。
プロセスを生成する方法、プロセス間のメッセージの送受信の基本的な構造を学ぶ。
spawn
新しいプロセスを生み出す基本的な方法は、自動でインポートされる`spawn/1
関数を使う。
iex> spawn fn -> 1 + 2 end
#PID<0.63.0>
spawn/1
はPID(プロセス識別子)を返すことに注意する。この時点、このプロセスは死んだ可能性が高い。スポーンされたプロセスは、与えられた関数を実行し、関数が完了したら終了する。
iex> pid = spawn fn -> 1 + 2 end
#PID<0.65.0>
iex> Process.alive?(pid)
false
試した環境によって、ここに記載したプロセス識別子とは異なるプロセス識別子になる
self/0
を呼び出すすことで現在のプロセスのPIDを知ることができる
iex> self()
#PID<0.59.0>
iex> Process.alive?(self())
true
プロセスは、メッセージの送受信ができるようになるともっと面白いものになる。
send and receive
send/2
を使ってプロセスにメッセージを送信することができ、receive/1
でそのメッセージを受信することができる。
iex> send self(), {:hello, "world"}
{:hello, "world"}
iex> receive do
...> {:hello, msg} -> msg
...> {:world, msg} -> "won't mathc"
...> end
"world"
プロセスにメッセージを送信した時、メッセージはプロセスのmailboxに保存される。receive/1
ブロックは、与えられたパターンにマッチするメッセージがないか現在のプロセスのmailboxを検索する。receive/1
は case/2
と同様に多くの節とガードが使える。
mailboxにパターンにマッチするメッセージがない場合、現在のプロセスはメッセージが到着するまで待ち続ける。この時、タイムアウトを指定することもできる。
iex> receive do
...> {:hello, msg} -> msg
...> after
...> 1_000 -> "nothing after 1s"
...> end
"nothing after 1s"
既にメッセージがmailboxにあることが期待できるときには、0のタイムアウトを与えることができる。
プロセス間でメッセージを送り合ってみる
iex> parent = self()
#PID<0.59.0>
iex> spawn fn -> send(parent, {:hello, self()}) end
#PID<0.85.0>
iex> receive do
...> {:hello, pid} -> "Got hello form #{inspect pid}"
...> end
"Got hello form #PID<0.85.0>"
上記の例は、1行目で現在のプロセス識別子をparent変数に入れ、2行目でspawn関数でparentへ{:hello, self()}を送信している。3行目以降で、recieve/1
を使い、mailboxに届いているメッセージを表示するという処理をしている。
シェル上では、flush/0
が有用かもしれない。これはmailboxのメッセージをすべて表示して消去する。
iex> send self(), :hello
:hello
iex> send self(), :world
:world
iex> flush()
:hello
:world
:ok
iex> flush()
:ok
Links
実際にスポーンする最も一般的な形式は、spawn_link/1
を介するもの。spawn_link/1
のサンプルを見る前に、プロセスが失敗した時に何が起きるか見てみる。
iex> spawn fn -> raise "oops" end
#PID<0.97.0>
iex>
17:08:39.964 [error] Process #PID<0.97.0> raised an exception
** (RuntimeError) oops
:erlang.apply/2
これは、単にエラーをログしただけで、プロセスをスポーンした方のプロセスはまだ動作し続けている。なぜならプロセス同士はお互いに独立しているため。もし、あるプロセスで失敗したことを他のプロセスへ伝えたい場合、これらのプロセスをリンクしなければならない。これはspawn_link/1
でできる。
iex> self()
#PID<0.59.0>
iex> spawn_link fn -> raise "oops" end
** (EXIT from #PID<0.59.0>) an exception was raised:
** (RuntimeError) oops
:erlang.apply/2
17:23:08.256 [error] Process #PID<0.62.0> raised an exception
** (RuntimeError) oops
:erlang.apply/2
Interactive Elixir (1.0.5) - press Ctrl+C to exit (type h() ENTER for help)
iex> self()
#PID<0.63.0>
シェルで失敗が起きた場合、シェルは自動的に失敗を補足し、それを表示する。上の例では、#PID<0.59.0>
プロセス上で、spawn_linkを使って、プロセスをスポーンし失敗を起こした。この時、プロセスをスポーンした方の#PID<0.59.0>
プロセスも、終了している。2度目のself()
ではプロセスIDが異なることから確認できる。
つまり、spawn_link/1
でスポーンしたプロセスが失敗すれば、リンクした親プロセスも落ちる。Process.link/1
を呼べば、リンクを任意のタイミングですることもできる。プロセスがどんな機能を提供しているかを知るために、Processモジュールを見ておくことを勧める。
プロセスとリンクは耐障害性システムを構築するのに重要な役割を担っている。Elixirのアプリケーションでは、プロセスがが死んでしまった時にそれを検知し、同じ場所で新しいプロセスを起動させるために、プロセスをスーパーバイザーにリンクさせる。これができるのは、プロセス同士は独立していて、デフォルトでは何も共有していないため。プロセスが独立していれば、プロセス内の失敗が他のプロセスをクラッシュさせたり状態を壊したりすることはない。
他の言語では、例外をキャッチしたり処理したりすることが求められるが、Elixirではプロセスが失敗するままで良い。これはスーパーバイザーがシステムを再起動していくれるため。"Failing fast"はElixirでソフトウェアを書く時には一般的な哲学。
Tasks
上記でプロセスをクラッシュさせた時、エラーメッセージがかなり貧弱だったことに気づいたかもしれない。
spawn/1
と spawn_link/1
関数では、エラーメッセージは直接仮想マシンで生成されるため、簡潔かつ詳細が欠けている。実際、開発者はTaskモジュール内の関数、 Task.start/1
と Task.start_link/1
を使うことでより明確に知ることができる。
iex> Task.start fn -> raise "oops" end
{:ok, #PID<0.61.0>}
18:04:19.248 [error] Task #PID<0.61.0> started from #PID<0.59.0> terminating
Function: #Function<20.54118792/0 in :erl_eval.expr/5>
Args: []
** (exit) an exception was raised:
** (RuntimeError) oops
(elixir) lib/task/supervised.ex:74: Task.Supervised.do_apply/2
(stdlib) proc_lib.erl:239: :proc_lib.init_p_do_apply/3
適切なエラーログを提供するだけではなく、 start/1
と start_link/1
に比べて、いくつかの違いがある。単にPIDを返すのではなく、{:ok, pid}
を返すことなど。これは監視ツリーでTaskを使えるようにするもの。さらに、Taskモジュールは Task.async/1
やTask.await/1
のような便利な関数と分散を簡単にする機能を提供する。
Mix and OTP guideでこれらの機能を探検する。より良いログを取得するために、Tasksを使うことを覚えておけばここでは十分。
State
これまで、状態についての話はなかった。もしアプリケーションを構築するときに状態が必要になった場合、例えばアプリケーションの設定やファイルをパーする必要があり、それをメモリに保持したい場合、どこに保存するのか。
プロセスがこの質問への最も一般的な答え。メッセージの送受信ができ、無限にループし、状態を保持することができるプロセスに記述できる。1つの例として、kv.exs
というファイル名で、key-valueストアとして動作する次のモジュールを書き、新しいプロセスを開始させてみる。
defmodule KV do
def start_link do
Task.start_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
start_link関数は、空のマップを持ってスタートするloop/1
関数を実行する新しいプロセスをスポーンしている。loop/1
関数はメッセージを待ち、各メッセージ毎に適切なアクションを実行する。
:get
メッセージの場合、callerにkeyの値のメッセージを送信して、loop/1
を再び呼び出し、新しいメッセージを待つ。一方、:put
メッセージは、keyとvalueを格納した新しいマップでloop/1
を呼び出す。
iex kv.exs
で動作させてみる。
$ iex kv.exs
iex> {:ok, pid} = KV.start_link
{:ok, #PID<0.65.0>}
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.63.0>}
iex> flush
nil
:ok
初めは、プロセスのマップはkeyを持っていない。そのため、:get
メッセージを送信して、現在のプロセスのmailboxをflushしてもnilが返ってくる。:put
メッセージを送信してみる。
iex> send pid, {:put, :hello, :world}
{:put, :hello, :world}
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.63.0>}
iex> flush
:world
:ok
上の例で、どうやってプロセスが状態を保持しているか、そしてどうやってメッセージを送ることで値の取得や更新ができるかがわかる。上記の例のpid
を知っていれば、どのプロセスからも、上のようにしてメッセージを送ることで、状態を操作することができる。
名前をつけpidを登録することもできる。こうすることで名前を知る全てのプロセスがメッセージを送ることができる。
iex> Process.register(pid, :kv)
true
iex> send :kv, {:get, :hello, self()}
{:get, :hello, #PID<0.63.0>}
iex> flush
:world
:ok
Elixirアプリケーションにおいて、プロセスに状態を持たせて名前を登録することはよく行われる。しかし、ほとんどの場合、上のように実装することはなく、Elixirが提供している多くの抽象物のうちのどれがを使う。例えば、状態を扱うagetnsというシンプルな抽象物を提供している。
iex> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.75.0>}
iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
:ok
iex> Agent.get(pid, fn map -> Map.get(map, :hello) end)
:world
Agent.start_link/2
へは :name
オプションを渡すこことができ、その場合は自動的に登録される。詳しくは、ドキュメントを参照する。