K.Sasada's Home Page

こめんとのついか

こめんとこめんと!

message

please set comment :).

_6(Thu)

Ruby VM アドベントカレンダーの 6 日目です.

本当はメソッド呼び出しの高速化の話の続きを書こうと思っていたのですが,予定を変えて非同期イベントについての話を書こうと思います.

とりあえず書いておくと,Ruby 2.0 から timeout(3){loop{}} のようなプログラムが終わらなくなりそうです(詳細は以下).ご自分のプログラムで,このような例が無いか確認してみて下さい(実際に,ご自分のプログラムを最新の trunk でテストして頂くのが早いです).もし,そのような例が互換性についてとても問題,ということになるようだったら,この挙動は変更される可能性があります.


timeout(30) { ... } と書いておくと,30秒後たってもブロックを実行中の場合,ブロックは Timeout::Error 例外により中断されます.

require 'timeout'
timeout(30){
  sleep 40
}
#=> t.rb:3:in `sleep': execution expired (Timeout::Error)

これはどうやって実現しているかというと,timeout ブロックを実行する時にスレッドをもう1つたてて,そのスレッドが 30 秒後にブロック実行中のスレッドに Thread#raise を使って Timeout::Error メソッドをあげます.30 秒以内にブロックを終了した場合は,そのタイムアウトを投げてくるスレッドを停止することで例外を発生させないようにします.

さて原理は簡単なのですが,これには次のような問題がありました.

require 'timeout'
timeout(30){
  begin
    ... # 処理 X
  ensure
    ... # 後始末処理
  end
}

このようなプログラムが起きたとき,処理 X の後で,必ず後始末処理が走ることが期待されます.例えば,ファイルを消したりだとか.

しかし,処理 X 中に Timeout::Error が発生したのなら良いのですが,後始末処理の最中に Timeout::Error が発生するとまずいことになります.

このような問題に対処するために,Thread.async_interrupt_timing というものが導入されました(名前は変更される可能性があります.というか,多分変更します).

面倒くさい話は置いといて,この場合どうやって使うかというと,Thread.async_interrupt_timing(Timeout::Error => :defer) { ... } としておくと,このブロックの部分では Timeout::Error 例外が来ても無視(ブロックから抜けたときは普通に例外があがります)となります(ちなみに,:defer というシンボル名も,変わる可能性があります.新しい仕様なので,まだ名前で悩んでいるのです).

逆に,Thread.async_interrupt_timing(Timeout::Error => :immediate) と指定すると,すぐに例外が上がることになります.

これを使って,後始末の最中に例外処理が起きないようにすると,次のように書きます.

require 'timeout'
Thread.async_interrupt_timing(Timeout::Error => :defer){
  # ここでは一切 Timeout::Error は受け取らない(中断しない)
  timeout(30){
    begin
      Thread.async_interrupt_timing(Timeout::Error => :immediate){ 
        ... # 処理 X.ここでは Timeout::Error によって中断される
      }
    ensure
      ... # 後始末処理.ここでは一切 Timeout::Error は受け取らない(中断しない)
    end
  }
}

こう書けば,後始末処理の中で Timeout::Error で中断されることがないため,安全に後始末を書くことが出来ます.

ちなみに,

require 'timeout'
timeout(30){
  begin
      ... # 処理 X.ここでは Timeout::Error によって中断される
    }
  ensure
    Thread.async_interrupt_timing(Timeout::Error => :defer){ 
      ... # 後始末処理.ここでは一切 Timeout::Error は受け取らない(中断しない)
    }
  end
}

こっちのほうが簡単でいいじゃないか,と言われるかもしれませんが,これだと ensure が始まった瞬間に Timeout::Error を受信して後始末処理がおこなわれなくなる,という危険があります.ちなみに,現在の MRI の実装では,このタイミングでは非同期イベントが起こりえないので,その心配はないのですが,一般的には気を付けるべきでしょう.

さて,毎回 timeout を書くごとにこのように冗長な記述をするのは面倒なので,みんな書かなくなる → 変なところで Timeout::Error が起こるようなプログラムになってしまう,という懸念があります.

そこで,[Bug #7503] make timeout.rb async-interrupt safe by default において,timeout の挙動を変えてしまおうという変更が小崎さんによってされています(これ,バグじゃないよなぁ).

先ほど,Thread.async_interrupt_timing では :never(例外をあげない) と :immediate(例外をすぐに上げる),という指定をしましたが,もう一つ,:on_blocking という指定があります(しつこいようですが,:on_blocking という名前は変更される可能性があります).

この :on_blocking という指定をすると,指定された例外を,主に I/O 処理まで遅延することが出来ます.外部からの例外によって中断させたい利用例の多くは I/O の中断なので,これで事足りる,という見方が出来ます.

[Bug #7503] の提案では,「timeout で中断できるブロックを,:on_blocking で囲んでおいて,中断は I/O 処理などのタイミングでしか出来ないようにしよう」というものになっています.このようにすることで,ensure での後始末などの間に割り込まれることを防ぐことが出来ます.

しかし,これには互換性に問題があり,例えば I/O 処理の含まない計算を timeout でデフォルトでは中断できないようになっています.

require 'timeout'
timeout(30){
  # 時間のかかる計算(I/O無し)
]

もしくは,無限ループを中断出来なくなります.

require 'timeout'
timeout(30{
  loop{} # 無限ループ
}

まぁ,後者をまともに書く人は居ないとは思いますが,前者はあり得るんじゃないの,という気がしています(なので,互換性100%という目標を掲げている Ruby 2.0 に入れて良いのか私はあんまり乗り気じゃないのです).

このように,従来通り即座に中断して欲しいときは,timeout(sec, immediate: true) のように呼べば良い,ということであり,書き直せばちゃんと動きます.「この書き直してね」といって許容できるかどうか,がこの修正の互換性的なキモになります.

というわけで,今回は次の3つのご紹介でした.

では今日はこの辺で.


初羽田空港国際線ターミナルで初台湾.この歳でもどんどん初めてのことが出来るってのはありがたいことだなぁ.いや,やったことがないことばかりなんだけどさ.

_tarui(Thu Dec 06 10:30:07 +0900 2012)

コメントのpreviewにだまされたので、もう一度投稿してみる。

多分、以下のようにする必要がー ensureでなんとかするようにしたいですね。 preview2では

# -*- coding: utf-8 -*-
require 'timeout'
e=Class.new(Exception)
Thread.async_interrupt_timing(e => :defer){
  # ここでは一切 Timeout::Error は受け取らない(中断しない)
  begin
    timeout(30,e){
      begin
        Thread.async_interrupt_timing(e => :immediate){
          ... # 処理 X.ここでは timeout によって中断される
        }
      ensure
        ... # 後始末処理.ここでは一切 timeout は受け取らない(中断しない)
      end
    }
    Thread.async_interrupt_timing(e=>:immediate){}
  rescue e
  end
}

trunk(バグってるかもしれない)では

# -*- coding: utf-8 -*-
require 'timeout'
e=Class.new(Exception)
timeout(30,e){
  begin
    Thread.async_interrupt_timing(e => :immediate){
      ... # 処理 X.ここでは timeout によって中断される
    }
  ensure
    Thread.async_interrupt_timing(e => :defer){     #blockingな処理をやる場合に必要
      ... # 後始末処理.ここでは一切 timeout は受け取らない(中断しない)
    }
  end
}

または

# -*- coding: utf-8 -*-
require 'timeout'
e=Class.new(Exception)
timeout(30,e){
  # preview2ではここでtimeoutによって中断される可能性がある
  Thread.async_interrupt_timing(e => :defer){     #previe2 or ensureでblockingな処理をやる場合に必要
    begin
      Thread.async_interrupt_timing(e => :immediate){
        ... # 処理 X.ここでは timeout によって中断される
      }
    ensure
      ... # 後始末処理.ここでは一切 timeout は受け取らない(中断しない)
    end
    }
  }
}

一番最後のコードはpreview2でもだいたい上手くうごきます。(つまりコメントで書いた場所を除いて)

_tarui(Thu Dec 06 11:49:28 +0900 2012)

 理由を書くのを忘れてました。つまり、timeoutのブロック内で発生する例外はTimeout::Errorではないからです。


好きにコメントを編集してください。ただし、あまり他の人のコメントを書き換えることは感心しません。



back

tton 記述が使えます。YukiWikiな記述してりゃ問題ありません。

「行頭に#code」 と、「行頭に#end」 で挟むと、その間の行は pre で囲まれます。プログラムのソースを書くときに使ってください。

例:

#code

(なんかプログラム書く)

#end

リンクは

[[なまえ|http://www.example.org]]

とか

[[http://www.example.org]]

で貼れます。

$Date: 2003/04/28 10:27:51 $