どうも、靖宗です。
一章一章が重くなってきた・・・
めげずに第十一章、プロセスのお話。
Elixirの内部ではコードはプロセス毎に実行される的な。
一個のコードにつき一プロセス?ただ、ここでいう1プロセスはOSの1プロセスではなく、別の記事でErlangのVM上で発行されるプロセスのことをさしていた気がします。
spawn
プロセスを発行する簡単な例としてはspawn/1
という関数を実行するみたいです。
iex(1)> spawn fn -> 1 + 2 end #PID<0.101.0>
おそらくスクリプト外でのプロセスを実行するときとかに使うんでしょう。
関数の返値としてプロセスIDが返ってきてますので、変数に格納してあとで煮るなり焼くなりできそうです。
iex(2)> pid = spawn fn -> 1 + 2 end #PID<0.103.0> iex(3)> pid #PID<0.103.0> iex(4)> Process.alive?(pid) false iex(5)>
自分のプロセスIDはself/0
で取得できるようです。
iex(5)> self() #PID<0.99.0> iex(6)> Process.alive?(self()) true
おそらくこのPIDを用いてメッセージを送ったり受け取ったりするんでしょうか。
send and receive
Elixirにはsend/2
とreceive/1
という関数があるようで、これらでプロセス間の通信をやるみたいです。
iex(7)> send self(), {:hello, "world"} {:hello, "world"} iex(8)> receive do ...(8)> {:hello, msg} -> msg ...(8)> {:world, msg} -> "won't match" ...(8)> end warning: variable "msg" is unused (if the variable is not meant to be used, prefix it with an underscore) iex:10 "world"
なんか怒られてますがwarningなので気にしない。
send
とreceive
のタイミングがずれてても問題無いところを鑑みるに、メッセージは一応バッファみたいなの(ここではthe process mailbox
と書かれている)に格納されてreceiveで取り出していけるみたい。
取り出したら消えるのか?試してみる
iex(1)> send self(), {:hello, "world"} {:hello, "world"} iex(2)> receive do ...(2)> {:hello, msg} -> msg ...(2)> {:world, msg} -> "won't match" ...(2)> end warning: variable "msg" is unused (if the variable is not meant to be used, prefix it with an underscore) iex:4 "world" iex(3)> receive do ...(3)> {:hello, msg} -> msg ...(3)> {:world, msg} -> "won't match" ...(3)> end warning: variable "msg" is unused (if the variable is not meant to be used, prefix it with an underscore) iex:5
固まったので消えるみたい。
さて、receive
ですが、doブロックを取っているのでcase/2
の様なパターンマッチの条件分岐がかけるようです。
また、タイミングずらして自分に送れてるところを鑑みると、メッセージの送信は非同期でいちいちプロセスが止まったりはしないようです。
先ほどは2回受信すると止まりましたが、タイムアウトも設定できるようです。
iex(1)> receive do ...(1)> {:hello, msg} -> msg ...(1)> after ...(1)> 1_000 -> "nothing after 1s" ...(1)> end "nothing after 1s"
1000を指定してるあたり、ミリ秒で指定出来るようです。
0を指定すると既に受け取ってる時にはreceive
が実行され、既に受け取ってない場合はafter
以降を実行するという使い方もできるみたいです。
自身のプロセス間だけでなく、別プロセスからのメッセージの送受信もやってみる。
iex(2)> parent = self() #PID<0.99.0> iex(3)> spawn fn -> send(parent, {:hello, self()}) end #PID<0.112.0> iex(4)> receive do ...(4)> {:hello, pid} -> "Got hello from #{inspect pid}" ...(4)> end "Got hello from #PID<0.112.0>"
spawn
の関数下でのself/0
がきちんと別プロセスIDになってる。
inspect/1
というのは、データ構造を文字列に変換する関数のようで、おそらくデバッグ用の関数と思われる。
spawn
とかでPIDが#PID<0.99.0>
のように表示されているが、この文字列に引数のデータを変換するという関数です。
flush/0
で現在受け取っているメッセージを表示できるようです。
iex> send self(), :hello :hello iex> flush() :hello :ok
なんか:ok
ってあるんだけどなんやこれ。気にしたら負けかな。
軽くぐぐっても出てこないので無視!
Links
なんかElixirではプロセスを発行するときはリンクするのが普通やで!とのこと。なんのこっちゃ。
サンプルを見てみる。
iex> spawn fn -> raise "oops" end #PID<0.58.0> [error] Process #PID<0.58.00> raised an exception ** (RuntimeError) oops (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
raise/1
で例外送出?っぽい。
他のプロセスで例外が出てもメインのプロセスは終わらない。まぁ独立してるので当然といえば当然ですよね。
例外を伝播させるにはspawn_link/1
を使うみたい。
iex(3)> self() #PID<0.99.0> iex(4)> spawn_link fn -> raise "oops" end ** (EXIT from #PID<0.99.0>) shell process exited with reason: an exception was raised: ** (RuntimeError) oops (stdlib) erl_eval.erl:678: :erl_eval.do_apply/6 19:09:04.483 [error] Process #PID<0.106.0> raised an exception ** (RuntimeError) oops (stdlib) erl_eval.erl:678: :erl_eval.do_apply/6 Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> self() #PID<0.107.0>
spawn_link/1
で例外が送出された場合は元のiexも再起動してプロセスIDが変わってます。
手動でプロセスをリンクさせるにはProcess.link/1
を使うみたい。一応やってみます。
iex(3)> Process.link(spawn fn -> raise "oops" end) ** (EXIT from #PID<0.107.0>) shell process exited with reason: an exception was raised: ** (RuntimeError) oops (stdlib) erl_eval.erl:678: :erl_eval.do_apply/6 19:13:19.170 [error] Process #PID<0.111.0> raised an exception ** (RuntimeError) oops (stdlib) erl_eval.erl:678: :erl_eval.do_apply/6 Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help) iex(1)>
特別な事が無い限りspawn_link/1
でいい気がします。
プロセス立ち上げっぱなしで、エラーで正常な動作をしていないケースなどを考えるとこの辺はきちんと管理しておくべきかもしれません。
プロセスが異常終了したかどうかをチェックできる関数などあればそれでも良いかもしれませんが、spawn_link/1
で対応できるところはこの関数でリンクしておくのがいいんじゃないでしょうか。
他の言語が例外処理を使うのに対してElixirではあんまり例外を受けてどうこうしないのが基本方針なんでしょうか。
とりあえずまだまだ先があるので進みます。
Tasks(タスク)
先ほどはspawn/1
などでプロセスを発行しましたが、ElixirではTasks
というモジュールでプロセスを発行するのが一般的みたいです。
iex(1)> Task.start fn -> raise "oops" end {:ok, #PID<0.114.0>} iex(2)> 11:16:41.917 [error] Task #PID<0.114.0> started from #PID<0.112.0> terminating ** (RuntimeError) oops (stdlib) erl_eval.erl:678: :erl_eval.do_apply/6 (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2 (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3 Function: #Function<20.128620087/0 in :erl_eval.expr/5> Args: []
なにやらゴチャゴチャと・・・
とりあえずここではTasks
はエラーの概要などを把握するにはspawn/1
などよりも便利に使える事だけ覚えとけ!って書いてるので、次。
State
コンフィグなどの状態を保持するにはどうしたらいいの?っていうベストプラクティス的な話でしょうか。
Elixirでは状態を保持するプロセスを無限に回し続けてメッセージで値を取り出したり入れたりするのが一つの手法のようです。
サンプルをやってみますが、まずは前準備でkv.exs
を作成します。
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
KV.start_link/0
で再帰の無限ループが開始されます。とは言ってもloop/1
というプライベート関数内でreceive
で一時停止しているので逐次処理が走って行く感じですね。
このkv.exs
をiex
で読み込ませていじっていきます。
PS > iex.bat .\kv.exs Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> {:ok, pid} = KV.start_link {:ok, #PID<0.104.0>} iex(2)> send pid, {:get, :hello, self()} {:get, :hello, #PID<0.102.0>} iex(3)> flush() nil :ok
:get
で値を取得しようとしてますが、まだなんにも登録してないのでnil
が返ってきてます。
:put
と値を送って見てみます。
iex(4)> send pid, {:put, :hello, :world} {:put, :hello, :world} iex(5)> send pid, {:get, :hello, self()} {:get, :hello, #PID<0.102.0>} iex(6)> flush() :world :ok
できました。
pidの値さえ知っていればどのプロセスでもメッセージは送受信できるので確かにこのやり方が適切かもしれません。
Process.register/2
を使えばアトムを使ってメッセージの送受信ができるようです。
iex> Process.register(pid, :kv) true iex> send :kv, {:get, :hello, self()} {:get, :hello, #PID<0.41.0>} iex> flush() :world :ok
プロセスIDは実行毎に変わってしまうので、いちいち全プロセスに現在のStoreのプロセスを教えることになるとおもいます。
そこでこのProcess.register/2
が生きてくるという感じでしょうか。
このStateとしてプロセスを発行するのは一般的な方法らしいのですが、直接的に使う事はなさそう?
Agent
という便利ツールがあるそうです。
iex> {:ok, pid} = Agent.start_link(fn -> %{} end) {:ok, #PID<0.72.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
ここだけ見ても特別便利そうには見えないですが、おそらくもっと進むにつれて色々便利さが分かってくるのでしょう!(たぶん)
MixとOTP云々と書いてますが、まだまだ先っぽい・・・(´・ω・`)