前回の はじめてなElixir(11) ではプロセスを使って状態を表現するのをやってみました。関数型言語で状態を表現するのに「関数の評価の途中の状態」を使ったとも言えるかと思います。
プログラミング Elixir の14章にしたがって、プロセス間のいろんな関係を使う練習をしてみましょう。ちなみに今回は惨敗です。きっと spawn
を spawn_link
にしたり spawn_monitor
に変えればすぐにできるんだろうなと甘く見てはじめましたが、うまく行きませんでした。せっかくですので晒しておきます。
プロセスが異常を起こすことを表現する
これまでは値の計算だけだったので(0で除算でもしない限り)プロセスが吹っ飛んでしまう心配はありませんでした。ここでは意図的に異常を起こしてみます。
if(Enum.random(0..9) == 0, do: exit(:boom))
って行をサーバプロセスに仕込んでみます。これで 1/10 の確率でプロセスが終了してしまいます。
defmodule Spawn9 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
pid = spawn(Spawn9, :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 "spawn9.ex"
[Spawn9]
iex(2)> Spawn9.test_sensor(0,10)
[0, 0, 3, 4, 3, 1, 1, 1, 1, 2]
iex(3)> Spawn9.test_sensor(0,10)
[0, 1, 1, 4, 4, 7, 7, 7, 9, 11]
iex(4)> Spawn9.test_sensor(0,10)
[0, 0, -1, -1, -1, -2, -4, -1, 2, 2]
iex(5)> Spawn9.test_sensor(0,10) # !ここで反応がなくなった
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
$
では、プロセスの異常を検出してみましょう。できないと不便でしょうがないですからね。
別のプロセスの立て方を試す (start_link)
プロセス同士でリンクするようにするには spawn 関数の代わりに spawn_link 関数を使うようです。上のプログラムで
pid = spawn(Spawn9, :pseudo_temp_sensor, [nil])
となってるのを
pid = spawn_link(Link9, :pseudo_temp_sensor, [nil])
としました。
defmodule Link9 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
pid = spawn_link(Link9, :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 "link9.ex"
[Link9]
iex(2)> Link9.test_sensor(0,10)
[0, -3, -4, -4, -4, -6, -6, -4, -7, -7]
iex(3)> Link9.test_sensor(0,10)
[0, -3, -6, -6, -5, -3, -6, -7, -7, -9]
iex(4)> Link9.test_sensor(0,10)
** (EXIT from #PID<0.101.0>) shell process exited with reason: :boom
Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
これは教科書によるとプロセス同士でリンクがなされ、一方のプロセスが終了した際にもう一方のプロセスも終了するようです。ここで「もう一方のプロセス」は iex の実行そのものですので iex が終了してしまってます。終了後に再起動されてるので iex(1)> と数字が 1 に戻ってます。
spawn_link でプロセスの終了を捕まえる
プロセスの終了を捕まえるのに
Process.flag(:trap_exit, true)
を使えば良いと教科書にありますので、入れてみました。
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)> c "link10.ex"
[Link10]
iex(2)> Link10.test_sensor(0,10)
### 反応なし…
あれえ、ハングアップしてしまいました。ただの spawn で書いたときに戻ってしまったかの如くです。教科書通りにやってると、こうなったときに途方にくれます。はて。
Kernel.exit/1 の代わりに Process.exit/2 を使ってみる
こういうときは頼るのはドキュメントです。というかそれしかないのでそれ見ます。するってえとこんなのに気が付きました。
https://hexdocs.pm/elixir/search.html?q=exit
今使ってる exit は Kernel.exit/1 関数です。これの他に Process.exit/2 関数があります。プロセスの終了を検出させるのは Process.flag 関数ですから Process モジュールの関数で統一してみましょう。
if(Enum.random(0..9) == 0, do: exit(:boom))
としていたのを以下に書き換えます。
if(Enum.random(0..9) == 0, do: Process.exit(self(), :boom))
defmodule Link11 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 -> mes
end
end
def test_sensor(zero_th, num) do
Process.flag(:trap_exit, true)
pid = spawn_link(Link11, :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 "link11.ex"
[Link11]
### 反応なし…
やはり、キャッチできませんでした。どうもよく分かりません。
教科書に戻ってみる
一旦、自分の関数を捨てて、教科書に忠実にやってみましょう。
defmodule Link3 do
import :timer, only: [sleep: 1]
def sad_function do
sleep 500
exit(:boom)
end
def run do
Process.flag(:trap_exit, true)
spawn_link(Link3, :sad_function, [])
receive do
msg ->
IO.puts "Message received: #{inspect msg}"
after 1000 ->
IO.puts "Nothing happened as far as I am concerned"
end
end
end
これを実行すると、想定通りの動作が起こります。
iex(1)> c "link3.ex"
[Link3]
iex(2)> Link3.run
Message received: {:EXIT, #PID<0.108.0>, :boom}
:ok
iex(3)>
はて自分の場合は何が悪いのか。
さらに別のプロセスの立て方を試す (monitor)
spawn_link はどうも謎めいていました。ただ、教科書を読み進めると、spawn_link より spawn_monitor の方が使う機会が多そうです。よくわからないままですが、こっちに進めることにします。spawn_link で懲りたのでまずは教科書に忠実に。
defmodule Monitor1 do
import :timer, only: [sleep: 1]
def sad_function do
sleep 500
exit(:boom)
end
def run do
res = spawn_monitor(Monitor1, :sad_function, [])
IO.puts inspect res
receive do
msg ->
IO.puts "Message received: #{inspect msg}"
after 1000 ->
IO.puts "Nothing happened as far as I am concerned"
end
end
end
これを実行すると予期したとおり以下が得られます。
iex(1)> c "monitor1.ex"
[Monitor1]
iex(2)> Monitor1.run
{#PID<0.109.0>, #Reference<0.3595623782.1760034817.114221>}
Message received: {:DOWN, #Reference<0.3595623782.1760034817.114221>, :process, #PID<0.109.0>, :boom}
:ok
はて、では自分で書いたプロセスを 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, {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)> c "monitor12.ex"
[Monitor12]
iex(2)> Monitor12.test_sensor(0,10)
** (ArgumentError) argument error
:erlang.send({#PID<0.109.0>, #Reference<0.476889410.3371171841.38929>}, {#PID<0.101.0>, {:init, 0}})
monitor12.ex:23: Monitor12.test_sensor/2
iex(2)>
と、プロセスは作れますが :init の呼び出しでエラーしておしまいです。spawn_monitor で生成したプロセスは呼び出し方が違うのか…
今日は疲れたのでおしまいにします。プロセスは erlang まで突っ込んで勉強しないと分からないゾーンですので、まだしばらくかかりそうな気配です。
参考文献(後で読む)
Erlangのマニュアル
すごいE本をElixirでやる(29)
リンクされたプロセスの終了時の挙動
修正履歴
2019.02.05 @CostlierRain464 さんの指摘で link10.ex のプログラムのハイライトがなかったのを修正しました。