今日は今度こそ spawn_link
と spawn_monitor
で並行プロセスを作りましょう。
[はじめてなElixir(12)いろんな方法で並行プロセスを作る(失敗編)] (https://qiita.com/kikuyuta/items/f861379706361beacc2c) というのを書いたら、Sapporo BEAM を主催なさってる @niku さんが [はじめてなElixir(12)でうまくいっていなさそうなところを説明する]
(https://qiita.com/niku/items/faafabb2d3ac0bf5847e#_reference-ea9d8896ccf4489fc526) という記事でどこがおかしいのを指摘して下さいました。
まあ、これをなぞればそれでおしまいなのですが、それだと覚えないので、「うまく行かなかったときにちゃんと挙動を見て、リファレンスに当たってればできてたはず」というのだけ心に留め置いて、もう一度やり直してみます。
もともとのお題のおさらい (spawn)
さて失敗に至る直前というと1週間前のもくもく会で、これは旨いことできた並行プログラミングでした。そのときの spawn
使った はじめてなElixir(11) のプログラムがこれです。
defmodule Spawn8 do
def pseudo_temp_sensor(temp) do
receive do
{sender, {:init, init_temp}} ->
send(sender, init_temp)
pseudo_temp_sensor(init_temp)
{sender, :next} ->
send(sender, temp)
cur = temp + Enum.random(0..3) * Enum.random(-1..1)
pseudo_temp_sensor(cur)
end
end
def test_next_temp(pid) do
send(pid, {self(), :next})
receive do mes -> mes
end
end
def test_sensor(zero_th, num) do
pid = spawn(Spawn8, :pseudo_temp_sensor, [nil])
send(pid, {self(), {:init, zero_th}})
receive do mes -> mes end
1..num |> Enum.map(fn(_void) -> test_next_temp(pid) end)
end
end
これを動かすとリストで結果が返ってきます。
iex(1)> c "spawn8.ex"
[Spawn8]
iex(2)> Spawn8.test_sensor(0, 10)
[0, 0, 1, 0, -3, -3, 0, 0, 2, -1]
iex(3)> Spawn8.test_sensor(0.0, 10)
[0.0, -1.0, -4.0, -4.0, -4.0, -7.0, -5.0, -2.0, -3.0, -1.0]
iex(4)> Spawn8.test_sensor(10.0, 10)
[10.0, 10.0, 10.0, 11.0, 11.0, 11.0, 14.0, 14.0, 14.0, 14.0]
iex(5)>
うまく動いています。
spawn_link を使ってみる
これの spawn
を spawn_link
にすれば、それだけでよろしく動くのではと動かしてみて、相棒プロセスの終了を捉えられるかと思えば捉えられなかった… のがこちら。
defmodule Link10 do
def pseudo_temp_sensor(temp) do
receive do
{sender, {:init, init_temp}} ->
send(sender, init_temp)
pseudo_temp_sensor(init_temp)
{sender, :next} ->
if(Enum.random(0..9) == 0, do: exit(:boom))
send(sender, temp)
cur = temp + Enum.random(0..3) * Enum.random(-1..1)
pseudo_temp_sensor(cur)
end
end
def test_next_temp(pid) do
send(pid, {self(), :next})
receive do mes -> mes
end
end
def test_sensor(zero_th, num) do
Process.flag(:trap_exit, true)
pid = spawn_link(Link10, :pseudo_temp_sensor, [nil])
send(pid, {self(), {:init, zero_th}})
receive do mes -> mes end
1..num |> Enum.map(fn(_void) -> test_next_temp(pid) end)
end
end
これを動かすとハングアップして何も返ってきません。
iex(1)> Link10.test_sensor(0, 10)
# ここで無反応
send と receive が何を返しているのか見てみる
これ関数を呼び出しても返り値を使ってない文だらけです。関数たちが何を返ってるのか見ると分かるかもかも。
defmodule Link13 do
def pseudo_temp_sensor(temp) do
receive do
{sender, {:init, init_temp}} ->
IO.inspect(":init #{send(sender, init_temp)}")
pseudo_temp_sensor(init_temp)
{sender, :next} ->
if(Enum.random(0..9) == 0, do: exit(:boom))
IO.inspect(":next #{send(sender, temp)}")
cur = temp + Enum.random(0..3) * Enum.random(-1..1)
pseudo_temp_sensor(cur)
end
end
def test_next_temp(pid) do
send(pid, {self(), :next})
receive do mes -> IO.inspect("test_next #{mes}")
end
end
def test_sensor(zero_th, num) do
Process.flag(:trap_exit, true)
pid = spawn_link(Link13, :pseudo_temp_sensor, [nil])
send(pid, {self(), {:init, zero_th}})
receive do mes -> IO.inspect("test #{mes}") end
1..num |> Enum.map(fn(_void) -> test_next_temp(pid) end)
end
end
link10.ex
の次が link13.ex
なのが泣かせますね。
これ実行するといろいろ出てきて「はあ、なるほど」となります。運が良いと(うまいこと途中まで exit しないで、でも最後まで行かずに exit してくれると)こんな風になります。
iex(7)> Link13.test_sensor(0,10)
":init 0"
"test 0"
":next 0"
"test_next 0"
":next 2"
"test_next 2"
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:EXIT, #PID<0.128.0>, :boom}
(elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir) lib/string/chars.ex:22: String.Chars.to_string/1
link13.ex:17: Link13.test_next_temp/1
(elixir) lib/enum.ex:1318: anonymous fn/3 in Enum.map/2
(elixir) lib/enum.ex:2967: Enum.reduce_range_inc/4
(elixir) lib/enum.ex:1930: Enum.map/2
これは exit したからエラーしてるんじゃなくて、exit の結果 receive do mes -> IO.inspect("test_next #{mes}")
で mes に返ってくる値を文字列にしようとして、でもできなくてエラーしてます。はい、receive の結果に入っていたんですね。それを捨ててました。
spawn_link 決定版
そうと分かれば対応は簡単です。receive の結果を受け取って処理するようにします。
defmodule Link14 do
def pseudo_temp_sensor(temp) do
receive do
{sender, {:init, init_temp}} ->
send(sender, init_temp)
pseudo_temp_sensor(init_temp)
{sender, :next} ->
if(Enum.random(0..9) == 0, do: exit(:boom))
send(sender, temp)
cur = temp + Enum.random(0..3) * Enum.random(-1..1)
pseudo_temp_sensor(cur)
end
end
def test_next_temp(pid) do
send(pid, {self(), :next})
receive do
{:EXIT, _pid, reason} ->
IO.inspect("exit by #{reason}")
exit(reason)
mes -> mes
end
end
def test_sensor(zero_th, num) do
Process.flag(:trap_exit, true)
pid = spawn_link(Link14, :pseudo_temp_sensor, [nil])
send(pid, {self(), {:init, zero_th}})
receive do mes -> mes end
1..num |> Enum.map(fn(_void) -> test_next_temp(pid) end)
end
end
相手のプロセスが exit したのを検出して自分も exit するようにしてみました。
iex(2)> Link14.test_sensor(0,10)
[0, 0, 0, 0, 0, 3, 2, 2, 2, 2]
iex(3)> Link14.test_sensor(0,10)
"exit by boom"
** (exit) :boom
link14.ex:20: Link14.test_next_temp/1
(elixir) lib/enum.ex:1318: anonymous fn/3 in Enum.map/2
(elixir) lib/enum.ex:2967: Enum.reduce_range_inc/4
(elixir) lib/enum.ex:1930: Enum.map/2
iex(3)>
実行してみると、最後までプロセスが exit しない場合にはリストを、途中で exit した場合は自分も exit してのが分かります。
spawn_monitor を使ってみる
spawn_link がうまく扱えたので、こんどは spawn_monitor に再挑戦です。前回失敗してたのは以下です。
defmodule Monitor12 do
def pseudo_temp_sensor(temp) do
receive do
{sender, {:init, init_temp}} ->
send(sender, init_temp)
pseudo_temp_sensor(init_temp)
{sender, :next} ->
if(Enum.random(0..9) == 0, do: Process.exit(self(), :boom))
send(sender, temp)
cur = temp + Enum.random(0..3) * Enum.random(-1..1)
pseudo_temp_sensor(cur)
end
end
def test_next_temp(pid) do
send(pid,pin{self(), :next})
receive do mes -> mes
end
end
def test_sensor(zero_th, num) do
pid = spawn_monitor(Monitor12, :pseudo_temp_sensor, [nil])
send(pid, {self(), {:init, zero_th}})
receive do mes -> mes end
1..num |> Enum.map(fn(_void) -> test_next_temp(pid) end)
end
end
これを動かすと、こんな風に失敗します。
iex(1)> Monitor12.test_sensor(0,10)
** (ArgumentError) argument error
:erlang.send({#PID<0.107.0>, #Reference<0.2562144121.1728053251.205548>}, {#PID<0.104.0>, {:init, 0}})
monitor12.ex:23: Monitor12.test_sensor/2
これを注意深く見てみます。
-
引数エラー :erlang.send とは言ってるけど、erlang まで降りて考えなくても、要はこのプログラムの send に起因しているのでは
-
実引数が出てるようなのでさらによく見る
- 実引数
{#PID<0.107.0>, #Reference<0.2562144121.1728053251.205548>}, {#PID<0.104.0>, {:init, 0}}
- 仮引数
(pid, {self(), {:init, zero_th}})
- 実引数
send の第一引数は pid のつもりで渡していますが、実際に渡っているのが {#PID<0.107.0>, #Reference<0.2562144121.1728053251.205548>}
と単純な pid になってないです。
ろろろろ、と思って pid に値を渡しているのは何かと見ると、これがくだんの spawn_monitor
です。spawn_monitor のマニュアルには
spawn_monitor(fun)
spawn_monitor((() -> any())) :: {pid(), reference()}
Spawns the given function, monitors it and returns its PID and monitoring reference.
とあり、返す値の型が「PID と reference」のタプル {pid(), reference()}
であることが分かります。はい、spawn や spawn_link と違って spawn_monitor が返すのは単なる pid じゃなかったんですね。
pid = spawn_monitor(Monitor12, :pseudo_temp_sensor, [nil])
となってたのを
{pid, _ref} = spawn_monitor(Monitor12, :pseudo_temp_sensor, [nil])
に書き換えましょう。こういうときパターンマッチで直せるのは楽ちんで良いですね。
spawn_monitor での exit の振る舞いを見る
それえと修正したプログラムを monitor13.ex と命名して動かすと見事に凍ります。そう、exit したときにどうなって何が返ってくるかは spawn_link と同じかどうか分かりませんよね。何が返ってくるのか見てみます。
defmodule Monitor14 do
def pseudo_temp_sensor(temp) do
receive do
{sender, {:init, init_temp}} ->
send(sender, init_temp)
pseudo_temp_sensor(init_temp)
{sender, :next} ->
if(Enum.random(0..9) == 0, do: Process.exit(self(), :boom))
send(sender, temp)
cur = temp + Enum.random(0..3) * Enum.random(-1..1)
pseudo_temp_sensor(cur)
end
end
def test_next_temp(pid) do
send(pid, {self(), :next})
receive do
mes -> IO.inspect("next #{mes}")
end
end
def test_sensor(zero_th, num) do
{pid, _ref} = spawn_monitor(Monitor14, :pseudo_temp_sensor, [nil])
send(pid, {self(), {:init, zero_th}})
receive do mes -> mes end
1..num |> Enum.map(fn(_void) -> test_next_temp(pid) end)
end
end
これを実行するとメッセージ mes に入るのが spawn_link とは違うことが分かります。
iex(1)> Monitor14.test_sensor(0,10)
"next 0"
"next 0"
"next 0"
** (Protocol.UndefinedError) protocol String.Chars not implemented for {:DOWN, #Reference<0.2533739201.4148166658.53916>, :process, #PID<0.106.0>, :boom}
(elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir) lib/string/chars.ex:22: String.Chars.to_string/1
monitor14.ex:18: Monitor14.test_next_temp/1
(elixir) lib/enum.ex:1318: anonymous fn/3 in Enum.map/2
(elixir) lib/enum.ex:2967: Enum.reduce_range_inc/4
(elixir) lib/enum.ex:1930: Enum.map/2
iex(1)>
要素数が5のタプル(ペンタプルっていうのかな)が返ってきてます。それも :EXIT
じゃなくて :DOWN
って言ってるし。
spawn_monitor 決定版
これにマッチするように receive の結果の受け取りを変更します。
defmodule Monitor15 do
def pseudo_temp_sensor(temp) do
receive do
{sender, {:init, init_temp}} ->
send(sender, init_temp)
pseudo_temp_sensor(init_temp)
{sender, :next} ->
if(Enum.random(0..9) == 0, do: Process.exit(self(), :boom))
send(sender, temp)
cur = temp + Enum.random(0..3) * Enum.random(-1..1)
pseudo_temp_sensor(cur)
end
end
def test_next_temp(pid) do
send(pid, {self(), :next})
receive do
{:DOWN, _void1, _void2, _void3, reason} ->
IO.inspect("exit by #{reason}")
exit(reason)
mes -> mes
end
end
def test_sensor(zero_th, num) do
{pid, _ref} = spawn_monitor(Monitor15, :pseudo_temp_sensor, [nil])
send(pid, {self(), {:init, zero_th}})
receive do mes -> mes end
1..num |> Enum.map(fn(_void) -> test_next_temp(pid) end)
end
end
これを実行してみます。
iex(1)> Monitor15.test_sensor(0,10)
"exit by boom"
** (exit) :boom
monitor15.ex:20: Monitor15.test_next_temp/1
(elixir) lib/enum.ex:1318: anonymous fn/3 in Enum.map/2
(elixir) lib/enum.ex:2967: Enum.reduce_range_inc/4
(elixir) lib/enum.ex:1930: Enum.map/2
iex(1)> Monitor15.test_sensor(0,10)
[0, -1, -1, -1, 1, 1, 0, 2, 2, 2]
iex(2)>
1回目が見事に引っかかって元の呼び出しプロセスが exit しています。2回目は子プロセスが最後まで exit しなかったので呼び出し側のプロセスが結果のリストを返しています。
ようやく1週間ほど前のリベンジができました。
参考文献
- elixir Documentation
- [はじめてなElixir(12)いろんな方法で並行プロセスを作る(失敗編)] (https://qiita.com/kikuyuta/items/f861379706361beacc2c)
- [はじめてなElixir(12)でうまくいっていなさそうなところを説明する]
(https://qiita.com/niku/items/faafabb2d3ac0bf5847e#_reference-ea9d8896ccf4489fc526)