『9.8 キープアライブプロセス』徹底理解(2)

先日の続き。

実は書いているうちに新たな疑問が浮かんできて、再度考え直していた。その結果、先日の内容には少し嘘が含まれていたことにも気付いた(詳細は「問題点4」の中で)。やっぱり「徹底検証」だとか格好つけるもんじゃないね!

問題点3. 同じNameでkeep_aliveを評価するのは競合ではない

「問題点2」の考察の中では即死するプロセスとsleepを使って意図的に競合状態を起こしてみたわけだが、本文中では次のような場合に競合が起こると説明されている。

そのような状況は2つのプロセスがNameに同じ値を指定して同時にkeep_aliveを評価しようとすると発生する。この状況は競合状態と呼ばれる。2つのコード(上記のコードと、on_exitの中でリンク操作を実行する部分)がお互いにリンク操作を取りあってしまう状態だ。ここで問題が起きると、プログラムは期待通りには動かないだろう。

しかしこれは変だ。複数のプロセスが同じ Name で keep_alive を呼び出せば確かにプログラムは正しく動かないだろうが、それは単に重複する名前で register/2 を呼んだからであって、競合状態とは何の関係もない。

  • register に失敗した場合 on_exit は評価されないのだから、リンク操作自体実行されない
    →「競い合う」要素がない
  • register に失敗したとき死ぬのは register の呼び出し元プロセスであって、登録されつつある当のプロセスではない
    → リンクの成否に関わらず、on_exit が意味をもつような状況にはならない

よってこの部分の説明は、単純に勘違いによって挿入されたものだと思われる。

問題点4. spawn と register が競合する可能性

競合が起こるのはむしろ spawn と register の方なのだ。すなわち

  Pid = spawn(Fun),
  register(Name, Pid),

この2つの文の間でプロセスが死んでしまうと、死んだプロセスを登録しようとして register がエラーを起こし、プログラムの実行が停止する。

……実は先日「問題点2」を検証する中で行った実験では、この競合が起こる可能性があった。もし競合が起こった場合 keep_alive が停止して、「1秒おきに延々と再起動が繰り返される」という説明は嘘になる。確認が足りなくて恐縮。

とはいえ、単に即死するプロセスを登録したくらいではなかなか再現されないようだ。

1> Pid = spawn(fun() -> 1 end), register(foo, Pid).
true

適当にスリープを入れたり別の処理を挟めば再現できる。

2> Pid2 = spawn(fun() -> 1 end), lib_misc:sleep(10), register(foo, Pid2).
** exception error: bad argument
     in function  register/2
        called as register(foo,<0.54.0>)

結局どうすれば良いのか?

競合が起こり得る組み合わせは少なくとも2つある。

  • spawn と link
  • spawn と register

これらをアトミックに実行するための仕組みを用意すれば良い。前者は組み込みの spawn_link であり、後者は演習8.11がヒントとなる。

私なりの on_exit と keep_alive の改良は次のようになった。

on_fun_exit(Fun, Handler) ->
    spawn(fun() ->
                  process_flag(trap_exit, true),
                  Pid = spawn_link(Fun),
                  receive
                      {'EXIT', Pid, Why} ->
                          Handler(Pid, Why)
                  end
          end).
 
keep_alive(Name, Fun) ->
    on_fun_exit(fun() ->
                        try register(Name, self()) of
                            true -> Fun()
                        catch
                            error:badarg ->
                                exit({already_running, Name})
                        end
                end,
                fun(_Pid, _Why) -> %io:format("~p exit with ~p, try to restart~n", [_Pid, _Why]),
                                   keep_alive(Name, Fun) end).

on_exit の中で spawn に換えて spawn_link を使っているので、対象プロセスが即死してもきちんと死因が採れる。また Fun は register の後に実行されるので、死んだプロセスを登録するようなこともない。またその副作用として、同じ名前で register を呼んだ場合のエラーも捕捉できるようになっている。

演習8.11の解答に相当する部分は、全面的にこちらのフォーラムの解答によっている。

Leave a Reply