LoginSignup
3
1

More than 5 years have passed since last update.

はじめてなElixir(14)いろんな方法で並行プロセスを作る(復活編)

Last updated at Posted at 2018-10-05

今日は今度こそ spawn_linkspawn_monitor で並行プロセスを作りましょう。

はじめてなElixir(12)いろんな方法で並行プロセスを作る(失敗編) というのを書いたら、Sapporo BEAM を主催なさってる @niku さんが はじめてなElixir(12)でうまくいっていなさそうなところを説明する という記事でどこがおかしいのを指摘して下さいました。

まあ、これをなぞればそれでおしまいなのですが、それだと覚えないので、「うまく行かなかったときにちゃんと挙動を見て、リファレンスに当たってればできてたはず」というのだけ心に留め置いて、もう一度やり直してみます。

もともとのお題のおさらい (spawn)

さて失敗に至る直前というと1週間前のもくもく会で、これは旨いことできた並行プログラミングでした。そのときの spawn 使った はじめてなElixir(11) のプログラムがこれです。

spawn8.ex
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 を使ってみる

これの spawnspawn_link にすれば、それだけでよろしく動くのではと動かしてみて、相棒プロセスの終了を捉えられるかと思えば捉えられなかった… のがこちら。

link10.ex
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 が何を返しているのか見てみる

これ関数を呼び出しても返り値を使ってない文だらけです。関数たちが何を返ってるのか見ると分かるかもかも。

link13.ex
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 の結果を受け取って処理するようにします。

link14.ex
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 に再挑戦です。前回失敗してたのは以下です。

monitor.12.ex
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 と同じかどうか分かりませんよね。何が返ってくるのか見てみます。

monitor14.ex
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 の結果の受け取りを変更します。

spawn_monitir15.ex
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週間ほど前のリベンジができました。

参考文献

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1