ささだです.

(2012/03/11 20:35), SASADA Koichi wrote:
>  開発会議で出た「非同期割り込みがこの先生き残るには」という議論を,とり
> あえず日本語でまとめました.まだあまりまとまっていない(すみません)の
> で,すみませんが日本語で出させて下さい&突っ込んで下さい.
> 
>  あとで英語化したいと思います(してくれると嬉しい).英語化できたら,今
> 後はそっちということで.

 こちら,すっかり放置していたんですが,なんとかしないとと思って reboot
します.すみません.指摘されている点を取り込みました.

田中さん:[ruby-talk:294917] で提案されていた API について,下記に疑問を
書いています.すみませんが教えて貰えませんか.

小崎さん:なんか突っ込みありますか.


その他,突っ込み歓迎します.結局,デフォルトはどうするべきなんだ.


----

用語:
・trap ハンドラ:trapで登録するシグナルが来たときに行うブロック
  trap(SIGINT){ ... } の ... 部分

・非同期割り込み:Thread#raise や trap ハンドラなど,意図しない
  イベントを,ここでは非同期割り込みと呼ぶ
 (他にもあったら教えて下さい)

・割り込みチェック:非同期例外があるかどうかチェックし,もしあれば
  例外を発生刺せたり trap ハンドラを実行したりする

・ブロッキング処理:I/O などで,ブロックするかもしれない処理.


概要:
 非同期割り込みをチェックするタイミングを制御する primitive を提供す
る.制御の種類は次の通り,

  0. なるべく頻繁にチェックする(これまで通りの動作)
  1. ブロッキング処理のタイミングだけチェックする
    2. チェックしない

 ただし.チェックする例外クラス(の祖先)は指定できる.なの
で,SignalException (を継承した Interrupt)は頻繁にチェックする,
すなわち Ctrl+C はだいたい効く,しかし,TimeoutError のような例外は安全
なところまで遅延して処理するといった挙動が容易に記述できる.

 また,1, 2 の場合に,ユーザによって強制的にどのような例外が来ているか
チェックできる仕組みを導入する.


背景:
 Thread#raise を使うと,スレッドに対していろんなちょっかいを出すことが
できる.また,trap(signal){...} とすると,任意のシグナルについて,非同期
に呼ばれるブロックを指定することができる.

  # 例1
  th = Thread.new{
    begin
      ...
    rescue NantokaError
      ...
    end
  }
  th.raise(NantokaError) #=> th に NantokaError を強制的に引き起こす


  # 例2
  q = Queue.new
  th1 = Thread.new{
    q << calc_in_algorithm1
  }
  th2 = Thread.new{
    q << calc_in_algorithm2
  }
  result = q.pop
  th1.raise(TerminateCalcError)
  th2.raise(TerminateCalcError)
  # アルゴリズム1,2 で計算して,どちらか先に答えが出たら,
  # もう1つのほうを止める


  # 例3
  trap(SIGINT){
    # 何か後処理
  }
  trap(SIGHUP){
    # 何か reload 処理
  }
  server_exec # サーバの処理

 なお,現在の割り込みチェックは,RUBY_VM_CHECK_INTS() で行っており,メ
ソッドの起動時,リターン時,前方へのジャンプ時,ブロッキング処理の前後で
行っている.


問題点:

 Thread#raise だとかが上がってくるタイミングは制御できないので,例えば
ensure 中で後処理をしていた場合に困る.

 例えば,例4 は timeout の実装(の簡略版)だが,yield で起動したブロッ
ク中で,ensure を用いて何か資源の後始末をしていたとしても,その後始末中
に(ensure 節実行中に)TimeoutError が発生してしまう可能性がある.

  # 例4
  def timeout(sec)
    timer_thread = Thread.new(Thread.current){|parent|
      sleep(sec)
      parent.raise(TimeoutError)
    }
    begin
      yield
    ensure
      timer_thread.stop # close thread
    end
  end

  timeout(3){
    begin
      f = open(...)  # open(...){|f| ...} でいいんだけど,まぁ例として
    ensure
      f.close
    end
  }

  では,ensure だけでいいのか,というと,それ以外にも問題がある.例えば
次に示す例5について考える.

  # 例5
  begin
    f = open(...)
  ensure
    f.close if f
  end

 open(...) で開いたものを,"f =" でローカル変数に代入が完了する前に割り
込まれたとき,f は nil のままなので ensure で close されない.この例の
File の場合は GC で閉じることも可能だが,解放を必須とする資源一般を考え
ると問題である.

 この点について,例えば,行末まで割り込みを許さない(行末だけチェックす
る),といった解決案も提案されたが,"f =" や "open(...)" がもっと複雑
だった場合,その解決では無理である(例えば,"foo.bar =" は複数行のメソッ
ドとして定義されている可能性がある).


提案:
 非同期割り込みをチェックするタイミングを制御するための仕組みを新設す
る.原案は [1] にあるとおり.ただ,名前については今後検討する.

 制御の種類は次の通り,
  0. なるべく頻繁にチェックする(これまで通りの動作)
  1. ブロッキング処理のタイミングだけチェックする
    2. チェックしない

 0 はこれまで通り.

 1 は,POSIX thread の cancellation point を参考にしている.長時間ブ
ロックする処理は Thread#raise やシグナルでキャンセルすることが可能.

 I/O 以外にも

  - 明示的なウェイト (Kernel#sleep)
  - 条件変数待ち (ConditionVariable#wait)
  - スレッド終了待ち (Thread#join)
  - プロセス終了待ち (Kernel#system, Process#wait など)

などが含まれる.

 2 は,一切チェックしない.完全に非同期割り込みセーフに処理することが可能.

 ただし.チェックする例外クラス(の祖先)は指定できるようにする.これに
より,例えば SignalException (を継承した Interrupt)は頻繁にチェックす
る,すなわち Ctrl+C はだいたい効く,しかし,TimeoutError のような例外は
安全なところまで遅延して処理するといった挙動が容易に記述できる.


議論:

・デフォルトをどのモード(前節 0〜2 のこと)にするか?
 ・モード 1 で困る人はどれくらいいるか?
 (いないならデフォルトこれでいいんでは?)
 ・計算スレッドは止めても困らないので,例2のような場合はこれまで
  通り止めたい,という,モード 0 を期待する例はある.

・ensure 実行時に自動的にモードを変更するか?

・ユーザによって例外を poll する処理は,例外を発生するか,
 それとも例外オブジェクトを取得できるようにするだけにするか?

・モードを変更する API をどのように設計するべきか?
 [1] では,次の様な API を提案している.
   * Thread.check_interrupt(klass)
   * Thread.blocking_interruptible(klass) { ... }
   * Thread.blocking_uninterruptible(klass) { ... }
   * Thread.delay_interrupt(klass) { ... }

が,この API は,非同期割り込みを常にチェックするモード 0 は気にしていな
い気がする.あと,delay_interrupt ってなんだっけ?


参考文献:
[1] Akira Tanaka "Re: Thread#raise, Thread#kill, and timeout.rb are
unsafe" ruty-talk (2008.3)
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/294917


謝辞:
 この議論は,3/11 10:00から開催されたRuby開発者会議で行われました.参加
者は田中さん,nahi さん,たるいさん,mrkn さん,skype 越しに小崎さん,中
田さん,sora さん,遠藤さんでした.朝もはよからありがとうございました.
 その後,突っ込みを受けた内容を入れました.

-- 
// SASADA Koichi at atdot dot net