読者です 読者をやめる 読者になる 読者になる

もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

Elixir 入門 その10 - Processes

Elixir

Process

  • Elixirではすべてのコードはプロセス上で動作している。プロセスはお互いに分離していて、メッセージパッシングでやりとりする
  • ElixirのプロセスはOSのプロセスと混同されるべきではない。Elixirのプロセスは多くの他のプログラミング言語のスレッドと異なり極めて軽量である。数千プロセスが同時に並行稼働していることも珍しくない。

spawn

  • 新しいプロセスを作成する場合、spawn/1を使う。実際には後述のspawn_link/1を使うことが多い。
  • spawn/1は別プロセスで実行するfunctionを引数に取り、PIDを返す
  • self/0を使うと現在のプロセスのプロセスのPIDを取得できる
iex> pid = spawn fn -> 1 + 2 end
#PID<0.65.0>

iex> self()
#PID<0.58.0>

iex> Process.alive?(self())
true

send and receive

  • send/2を使うと別のプロセスにメッセージを送ることができる。受け取り側はreceive/1で受け取ることができる
# 自分にメッセージを送る
iex> send self(), {:hello, "world"}
{:hello, "world"}

# メッセージを受け取った場合の処理
# 送る前にreceiveをすると受け取るまで待ち続ける
iex> receive do
...>   {:hello, msg} -> msg
...>   {:world, msg} -> "won't match"
...> end
"world"

# タイムアウトを設定することも可能
iex> receive do
...>   {:hello, msg}  -> msg
...> after
...>   1_000 -> "nothing after 1s"
...> end
"nothing after 1s"

# flush/0を使えばmailboxに滞留しているメッセージを解放できる
iex> send self(), :hello
:hello

iex> flush()
:hello
:ok

# receive待ちのメッセージが無ければ:okのatomだけが返る
iex> flush()
:ok
  • プロセスにメッセージを送る際、メッセージはそのプロセスのmailboxに保存される。receive/1ブロックは与えられたパターンにマッチするまでmailboxを検索し続ける。
  • flushを使うと滞留しているメッセージをすべて解放する。

Links

  • spawn/1でプロセスを作成した場合、例外が起きてもプロセスは止まらない。spawn_link/1を使えば例外が起きたことを別のプロセスに伝えることができる
# 下記のコードをexsファイルで保存して実行するとraise oopsの時点でプロセスが終わる
spawn fn -> raise "oops" end

receive do
  :hello -> "let's wait until the process fails"
end
# 下記のコードをexsファイルで保存して実行するとraise oopsの時点でプロセスが終わった上で例外が起きたことが親プロセスに伝わる。
spawn_link fn -> raise "oops" end

receive do
  :hello -> "let's wait until the process fails"
end
  • Elixirでは他の言語のようにtry/catchで例外を制御するのではなくプロセスが死んだら積極的に死なせて新しいプロセスを立ち上げなおすのがよい。"Failing fast" はElixirアプリケーションを書く時の共通の哲学。

Tasks

  • spawn あるいは spawn_linkが出力するエラーメッセージはVMにより生成されたメッセージなので短くまとまっている。
  • Task.start/1 あるいは Task.start_link/1を使うとより詳細なメッセージが出る
  • returnもPIDではなく{:ok, PID}が返る
  • Taskモジュールは他にもasync/awaitのような便利な機能も提供している
iex(2)> spawn_link fn -> raise "oops" end

23:18:09.645 [error] Error in process <0.79.0> with exit value: {#{'__exception__'=>true,'__struct__'=>'Elixir.RuntimeError',message=><<4 bytes>>},[{erlang,apply,2,[]}]}


** (EXIT from #PID<0.72.0>) an exception was raised:
    ** (RuntimeError) oops
        :erlang.apply/2


# spawnに比べてより詳細なメッセージが出力される
Interactive Elixir (1.0.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Task.start_link fn -> raise "oops" end
** (EXIT from #PID<0.80.0>) an exception was raised:
    ** (RuntimeError) oops
        (elixir) lib/task/supervised.ex:74: Task.Supervised.do_apply/2
        (stdlib) proc_lib.erl:237: :proc_lib.init_p_do_apply/3


23:18:21.901 [error] Task #PID<0.82.0> started from #PID<0.80.0> terminating
Function: #Function<20.90072148/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:237: :proc_lib.init_p_do_apply/3
Interactive Elixir (1.0.5) - press Ctrl+C to exit (type h() ENTER for help)

State

  • 状態を保持し続けたい時はプロセスを無限にループさせながらMapに登録する。
  • Agentモジュールを使うと楽
# Agentに状態を保存するためのプロセスを登録する
iex> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.60.0>}

# 取得したPIDに別名を与える
iex> Process.register(pid, :kv)

# 登録したプロセスIDを指定してMapにkey-valueを登録する
iex> Agent.update(:kv, &(Map.put(&1, :hello, :world)))
:ok

# 登録したプロセスIDとkeyを指定すると対応するvalueが取れる
iex> Agent.get(:kv, &(Map.get(&1, :hello)))
:world