ささだです.

 開発会議で出た「非同期割り込みがこの先生き残るには」という議論を,とり
あえず日本語でまとめました.まだあまりまとまっていない(すみません)の
で,すみませんが日本語で出させて下さい&突っ込んで下さい.

 あとで英語化したいと思います(してくれると嬉しい).英語化できたら,今
後はそっちということで.



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

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

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

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


概要:
 非同期割り込みをチェックするタイミングを制御する primitive を提供す
る.制御の種類は次の通り,
  0. なるべく頻繁にチェックする(これまで通りの動作)
  1. ブロッキング処理のタイミングだけチェックする
    2. チェックしない
 ただし.チェックする例外クラス(の祖先)は指定できる.
 なので,SignalException (を継承した Interrupt)は頻繁にチェックする,
すなわち Ctrl+C はだいたい効く,しかし,TimeoutError のような例外は安全
なところまで遅延して処理するといった挙動が容易に記述できる.


背景:
 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. ブロック I/O のタイミングだけチェックする
    2. チェックしない

 0 はこれまで通り.

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

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

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


議論:

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

※ええと,すみません,いろんな議論があったと思いましたが,誰か補完して下
さい....


余談(というか TODO):
 rb_blocking_region() 時に強制的に終了時に CHECK_INTS() しているのはけ
しからんので,CHECK_INTS() を呼び出し側でさせるバージョンを作るべきである.

 例えば,

  blocking_region{
    data = read(...)
  }

  とあったとき,data が到達しているのならば,割り込んで欲しくないはずで
ある.ただし,data が到達する前に割り込みで中断していれば,CHECK_INTS()
により例外を発生させるべきである.この判断は rb_bocking_region() 利用者
しかできないので,その判断をさせるべきである.


参考文献:
[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