2007年6月4日月曜日

Threads on Rails

Rails application 上でアプリケーションから独立した処理を設けたくなった。
時間がきたら何かを始めるような、例えばリモートのファイルの更新を調べて
更新があればその変更をローカルに反映させる、といったことがやりたい。

後に http://www.igvita.com/blog/2007/03/29/scheduling-tasks-in-ruby-rails/
で OpenWFEru, BackgrounDRB などのライブラリがあるのを見つけるが、今回は
まず Ruby の Thread でどの程度できるかを確かめたかった。Java でも
このような用途への Scheduler は Quartz などいろいろあったが、たいていの
ことは Thread を拡張して済ますこともできたからである。

Ruby Thread の情報を探していて気になったのは、どうやらこれは Ruby ないの
ユーザースレッドとして実装されているということだ。言い方をかえると OS から
は単一スレッドのプロセスに見えるが、Ruby 内部で自前で処理を切り替えていると
いうことだ。OS のネイティブスレッドにマップされているわけではない。Java
1.1 の頃の懐かしの green thread と同じ原理だ。スレッドのセマンティックは
使えるが、それぞれが本当に平行して走れるわけではないため、計算主体の
処理をスレッド化しても処理時間が短くなるわけではなく、むしろスケジューリング
のオーバーヘッドも加わって少し遅くなってしまうだろう。マルチコア全盛?
の昨今、早期のネイティブスレッド対応が望まれる。Java thread はたいてい
ネイティブスレッド対応なので jruby では案外簡単に実現できるのかも
知れない。Java の Thread クラスをラップすればいいだけだから。

Rails は相変わらず development mode でしか使用していないが、ファイルを
書き換えて、再アクセスするだけですむのはやはり便利で、スレッドを持たせ
てもこれでいけるか実験してみた。

Thread.new の戻り値を controller のスタティック変数に代入してみる。
スレッドのボディは単にスリープとカウンタの表示をするだけだ。コントローラ
を書き換え再アクセスすると新しいスレッドが作られるが、古いスレッドも
依然として動き続ける。定義は書き換えたのにだ! 短絡適にデストラクタや
スタティックデストラクタを思いついたが、いずれも Ruby のリファレンスには
見当たらない。クラスのアンロードのイベントやそれに伴ったハンドラの
呼び出しもざっとみたところでは見当たらない。スタティック変数は最ロード
された際に初期化されているので、あとから古いスレッドを触ることもできないようだ。

試行錯誤と妥協の後に、Thread.new の結果を外部クラスのスタティックフィールドに
保持し、新規スレッド生成時にという録されていたスレッドに Thread.exit する
事を思いつき、動作することを確認した。

小技の覚書

* 外部クラスの利用

Thread を覚えておいてあとでとめられるクラスを models の下に作ったが、
クラスが認識されずエラーになった。どこかでコントローラとモデルの関連
づけの記述があったような、なかったような... とりあえずは
require 'models/mythreadmanager' のようにして利用できるようになった。
あと、この require しているソース内にエラーがあった場合、rails の
エラー画面にはその詳細が出てこないようだ。このあたりは設定で表示
されるようにできるのかも知れないが、単体でデバッグして回避。

* Thread のイディオム


begin
@@threada = Thread.new do
# print "D: thread a2 starting...\n"
icnt = 0
while true
#print "thread a: " + Thread.current.to_s + " icnt " + icnt, "\n"
# printf "D: thread a2: %s icnt %d\n", Thread.current.to_s, icnt
sleep 10
icnt += 1
end
end

MyThreadManager.register @@threada
rescue
print "@@threada rescue\n"
p $!
end


基本的に begin/rescue の中で行う。そうしないと黙ってぬけられても
気付かない。

rescue は catch のように例外ごとに複数設けることもできるが、例外クラスを
定義しなければならない。$! は短いメッセージしか出さないように見えたので
例外そのものは取れないかと調べてみると rescue => evar とすることで
拾えることに気付くが、後に $! 自体が例外オブジェクトであり、$! を使って
スタックとレースも出せることに気付く。

* logger の使用

require 'logger' して次の2行程度で commons-logging みたいな機能が利用
できるようになる。


@@log = Logger.new STDOUT
@@log.level = Logger::WARN


* private 指定

どこかで private 指定は C++ のように private: 以下に書いたものと
漠然と覚えていたが、普通に定義して

private :checkLocalDir 

と private に続けてメソッド名のシンボルを書くことで private に
なるようだ。

* nil 地獄

クラスのフィールドが増えてくると to_s のオーバーライドの中や
ちょっとした if 文などで nil を参照したと言うことでエラーに
なる。少しうるさすぎる気がしないでもない。

* %r

正規表現に / が多く現れる場合、vi や perl などでは / を ! で
代用できる。これは ruby ではそのままでは動かないが
%r!/{1,}$! とする事で実現できる。

* unit test

require 'test/unit' して Test::Unit::TestCase のサブクラスを
作れば、テストクラスができる。テスト用のメソッドは test_ で
はじめる。setup の用途も JUnit と同じ。

* Queue

Queue クラスはスレッド間の連携に便利。何でも渡せそう。

* 外部コマンドの起動

外部コマンドを使わざる終えない状況で、以下にうまくそれをコントロール
できるかというのが問題になってくる。ポイントとしては子プロセスとの
標準入出力、エラーを用いた通信と終了ステータスの確認だ。

open("|sort", "r+") のようにすると返された IO に対して書き込み、読み込み
できる。本当に簡単な処理であればこれでいいが、エラー出力は得られないし、
コマンドのステータスも得られない。これにかんして Open3 モジュールを
使うと stdin, out, err ともに得られることがわかるが、Open3.popen3 は
内部で fork を使用している。これを使ってみてわかったが windows 版では
fork がサポートされていないため動かない。open("|comm...") はどうして
動くのかということになるが、調べる気が起きなかった。

結局、次のようにコマンドへの入出力はリダイレクトを使って戻り値は
system の戻り値をつかう方法が無難そうだし、windows でも動作した。


r = system('mysql -u test --password=test test <a.sql >out 2>err')

p r # true for success, false for error
p $? # additional info on failure

1 件のコメント:

Unknown さんのコメント...

AP4Rはどうでしょうか?