dRubyのアプリケーションを雑に作るため、別プロセスに貸しだしたオブジェクトを一定時間、GCから保護するTimerIdConvっていうのがあります。*1
保護するのは簡単なんだけど、保護するのをやめるのがめんどくさい。とりあえずスレッドを使って時々忘れさせるようにしておきました。想定される使い方では、dRubyでサービスを提供するプロセスは終了までそのまま提供するのでそうなってます。
でもできればこういうスレッドなくしてみたい。
で、どうでもいい仕事(この場合は忘れる処理)をするトリガーとしてGC使えないだろうかと考えました。
module OnGC module_function def on_gc(&blk) ObjectSpace.define_finalizer(Object.new, &blk) end end
Object.newしたオブジェクトがGCされたとき、ブロックを呼び出すことができます。
def do_gc p [:do_GC, Thread.main, Thread.current] end OnGC.on_gc { do_gc } GC.start puts "end."
実行する。
$ ruby on_gc.rb end. [:do_GC, #<Thread:0x007fccf207cd00 dead>, #<Thread:0x007fccf207cd00 dead>]
あれ?そこか。GC二回してみる。
def do_gc p [:do_GC, Thread.main, Thread.current] end OnGC.on_gc { do_gc } GC.start GC.start puts "end."
end.の前にログがでた。GCがいつ行われるのか、あてにできないってことだよね。
$ ruby on_gc.rb [:do_GC, #<Thread:0x007fed14080d00 run>, #<Thread:0x007fed14080d00 run>] end.
ループしてみる
定期的な処理をしたいので、ループさせてみる。
def do_gc p [:do_GC, Thread.main, Thread.current] OnGC.on_gc { do_gc } end
と、無限ループしてしまう。プロセスの終わりにGCが走るからじゃなくてファイナライザが働くので無限ループに入ってしまう。
中田さん情報でThread.main.alive?がfalseになるっぽいので終端条件をいれよう。
module OnGC module_function def on_gc(&blk) return unless Thread.main.alive? ObjectSpace.define_finalizer(Object.new, &blk) end end
どのスレッドで動くのか
なんとなくGC用のスレッドがあるのかと思ったけど、そうではない。どのスレッドでもGCが発生する可能性がある(と思う)。この実験では明示的なGC.startだけど、いつでも起きるよね。
def do_gc p [:do_GC, Thread.current] OnGC.on_gc { do_gc } end OnGC.on_gc { do_gc } th = Thread.new do 10.times do GC.start sleep(rand) end end 10.times do GC.start sleep(rand) end th.join puts "end."
できそうにないことを確認する
Threadは作れない?
def do_gc p [:do_GC, Thread.current] begin Thread.new { puts 'helo' } rescue p $! end OnGC.on_gc { do_gc } end OnGC.on_gc { do_gc } th = Thread.new do 10.times do GC.start sleep(rand) end end 10.times do GC.start sleep(rand) end th.join puts "end."
なんとつくれた。Thread.main.alive?がfalseのときはThreadErrorになる。
なるほどー。
Thread通信のしかけも動くかな。Queueで試してみる。
require 'thread' $queue = Queue.new def do_gc p [:do_GC, Thread.current] begin puts $queue.pop rescue p $! raise($!) end OnGC.on_gc { do_gc } end OnGC.on_gc { do_gc } th = Thread.new do 10.times do |n| GC.start $queue.push(n) sleep(rand) end end 10.times do |n| GC.start sleep(rand) end th.join puts "end."
動く(よびだせる)。でもこう書くと最後にはデッドロックで終わってしまう。だいたいどのスレッドでGCが働くかわからないのに、スレッドの待ち合わせするのはだめだよな。その手の操作は別スレッドを作ってやるべきだな。そうする。
def do_gc p [:do_GC, Thread.current] Thread.new do begin puts $queue.pop rescue p $! raise($!) end end OnGC.on_gc { do_gc } end
だいたい定期的な処理
do_gcで時刻を調べて、期限を過ぎていたら特別な処理をするようにすれば定期的な処理が書けそう。
$expires = Time.now + 2 def do_gc if $expires < Time.now p [:do_gc, Time.now] $expires = Time.now + 2 end OnGC.on_gc { do_gc } end OnGC.on_gc { do_gc } th = Thread.new do 30.times do |n| GC.start sleep(rand) end end 30.times do |n| GC.start sleep(rand) end th.join puts "end."
大団円
TimerIdConvのケースでは、こんな風に使おうと思います。
- keeperスレッドをやめる。(これはすでにやめている)
- @expiresをon_gcのループの制御に使う。次にrotateする時刻を入れる。nilなら停止。
- addのケースにだけ、on_gcのループの開始する
def invoke_keeper return if @expires @expires = Time.now + @keeping on_gc end
ループは同時にただ一つだけにしておきたいので、停止中の場合にだけ、開始する。
def on_gc return unless Thread.main.alive? return if @expires.nil? Thread.new { rotate } if @expires < Time.now ObjectSpace.define_finalizer(Object.new) {on_gc} end
まず、プロセス終了中、ループ終了状態をチェックする。次に、rotate時刻を過ぎていたら、別スレッドでrotateする。次回に備えて、on_gcのループの準備をする。ファイナライザーの登録以外、なにも状態を変えないのがミソ。rotateは別スレッドで実行され、状態の変更はsynchronizeの内側で行います。
def rotate synchronize do if @expires &.< Time.now @gc = @renew # GCed @renew = {} @expires = @gc.empty? ? nil : Time.now + @keeping end end end
rotateでは、@expiresを過ぎていたら、GCから保護している箱(Hash)を入れ替え、次のrotateの時刻を設定する。箱がからならnilにして、on_gcのループをやめる。
空になってon_gcのループが終わっても、つぎのaddのときにはinvoke_keeperされて再開される(と期待してる)。commitしていいかなあ。
あわせて読みたい
- 作者: 関将俊
- 出版社/メーカー: アスキー
- 発売日: 2001/10
- メディア: 単行本
- クリック: 2回
- この商品を含むブログ (10件) を見る
The dRuby Book: Distributed and Parallel Computing with Ruby
- 作者: Masatoshi Seki,Makoto Inoue
- 出版社/メーカー: Pragmatic Bookshelf
- 発売日: 2012/03/19
- メディア: ペーパーバック
- クリック: 1回
- この商品を含むブログ (24件) を見る
- 作者: 関将俊
- 出版社/メーカー: オーム社
- 発売日: 2005/07
- メディア: 単行本
- 購入: 1人 クリック: 41回
- この商品を含むブログ (144件) を見る
*1:慣れてくるとこれしなくてもうまく書けるので、自分は使ってません