技術メモ

プログラミングとか電子工作とか

Elixir入門(第十一章 プロセス)

f:id:ysmn_deus:20190122112104p:plain

どうも、靖宗です。
一章一章が重くなってきた・・・
めげずに第十一章、プロセスのお話。

Elixirの内部ではコードはプロセス毎に実行される的な。
一個のコードにつき一プロセス?ただ、ここでいう1プロセスはOSの1プロセスではなく、別の記事でErlangVM上で発行されるプロセスのことをさしていた気がします。

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/2receive/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なので気にしない。
sendreceiveのタイミングがずれてても問題無いところを鑑みるに、メッセージは一応バッファみたいなの(ここでは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.exsiexで読み込ませていじっていきます。

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云々と書いてますが、まだまだ先っぽい・・・(´・ω・`)