(ruby)(rubyにタイマーを実装してみようかと考えている所)
一定時間ごとにルーチンを呼び出すタイマー処理は
監視をするプログラムを作る時に重宝しますよね
rubyにはそういうタイマーが実装されていないみたいなので
自前で書くとどうなるのかを考えてみました
gemとか使わずに標準的なライブラリであるのなら誰か教えてください 笑)
まずはクラスの定義ですが
「&callback_block」の所があまり見慣れない人が居るかと思います
これはクラスをインスタンシングする時に
Timer.new(1){ puts('hello') } などとしますが
initializeの最後の引数名に &を付けることでその引数に
{ } の中のブロック部分を受ける事が出来ます
そのブロックを部分を実行するには callメソッドを使います
今回の例で言えば @callback_block.call() の所になるわけですね
では、実際に使ってみるところです
よくあるタイマー処理だと、まず一つコールバック関数を定義して
それとタイマーを関連付ける事で定期的にコールバック関数が実行されるようにします
今回の場合はそれと少し違って
クラスをインスタンシングする時にブロックで定義します
何言ってるかよく分からないでしょうから 笑)、サンプルです
実際は sleep(0.1)の所にコールバックされる処理を書きます
(ここでは処理に掛かった時間をsleepで想定してます)
ただし、この書き方だとコールバック処理に掛かった時間がインターバルに加わります
まぁ、コールバック部分の重複実行をしない方が良い場合は
この書き方のほうが良いでしょうね
もしも、ほぼ指定したインターバル通りに処理をしたい場合は
ただ、指定したインターバルよりもコールバック処理の時間が長くなってしまうと
一定量のスレッドが溜まってしまうという現象が起こります
(コールバック処理がインターバルより圧倒的に早く終わるならそのような事はありません)
また当然の事ながらコールバック処理の重複実行も発生します
よってこのような書き方をする場合は
そのことを想定したコーディングが必要になり、難易度が上がります
スレッドが溜まってしまう可能性がある場合
プログラムを終了する時に
まだスレッドが実行中であっても強制的にkillされてしまう可能性があります
もしも途中でkillされた時に支障が起こりうる場合は
(timeoutは ここでは 0.1秒単位なので デフォルトでは20秒になります)
終了処理中に呼び出すようにします
例えば、exit(0) の所だと
スレッドが強制的にkillされてしまうのを回避できるかもしれません
さて、インスタンシングしたTimerですが
実際にタイマーをスタート、ストップさせるには
あとは・・・、単に インスタンシングしただけではスクリプトの最後まで来ると
否が応でもプログラムが終了してしまいますから
メインルーチンの流れの最後のところで、なんらかのループ処理を書く必要があります
メインのループルーチンでも何かしたいのなら
loopやwhileなんかで
では、一通りこれらをまとめたサンプルを書いて終わります
監視をするプログラムを作る時に重宝しますよね
rubyにはそういうタイマーが実装されていないみたいなので
自前で書くとどうなるのかを考えてみました
gemとか使わずに標準的なライブラリであるのなら誰か教えてください 笑)
まずはクラスの定義ですが
class Timer public def initialize(interval, &callback_block) @loop_enable = false @interval = interval @callback_block = callback_block end def start() @loop_enable = true loop_start() end def stop() @loop_enable = false end private def loop_start() Thread.new(){ while @loop_enable @callback_block.call() sleep(@interval) end } end endといった感じかなぁと
「&callback_block」の所があまり見慣れない人が居るかと思います
これはクラスをインスタンシングする時に
Timer.new(1){ puts('hello') } などとしますが
initializeの最後の引数名に &を付けることでその引数に
{ } の中のブロック部分を受ける事が出来ます
そのブロックを部分を実行するには callメソッドを使います
今回の例で言えば @callback_block.call() の所になるわけですね
では、実際に使ってみるところです
よくあるタイマー処理だと、まず一つコールバック関数を定義して
それとタイマーを関連付ける事で定期的にコールバック関数が実行されるようにします
今回の場合はそれと少し違って
クラスをインスタンシングする時にブロックで定義します
何言ってるかよく分からないでしょうから 笑)、サンプルです
t1 = Timer.new(0.01){ sleep(0.1) }newの引数はインターバルタイムです、単位は秒なので 10msですね
実際は sleep(0.1)の所にコールバックされる処理を書きます
(ここでは処理に掛かった時間をsleepで想定してます)
ただし、この書き方だとコールバック処理に掛かった時間がインターバルに加わります
まぁ、コールバック部分の重複実行をしない方が良い場合は
この書き方のほうが良いでしょうね
もしも、ほぼ指定したインターバル通りに処理をしたい場合は
t1 = Timer.new(0.01){ Thread.new(){ sleep(0.1) } }のようにしてスレッドを作って実行すればいけますね
ただ、指定したインターバルよりもコールバック処理の時間が長くなってしまうと
一定量のスレッドが溜まってしまうという現象が起こります
(コールバック処理がインターバルより圧倒的に早く終わるならそのような事はありません)
また当然の事ながらコールバック処理の重複実行も発生します
よってこのような書き方をする場合は
そのことを想定したコーディングが必要になり、難易度が上がります
スレッドが溜まってしまう可能性がある場合
プログラムを終了する時に
まだスレッドが実行中であっても強制的にkillされてしまう可能性があります
もしも途中でkillされた時に支障が起こりうる場合は
def wait_threads_stop(number_of_thread = 1,timeout = 200) puts("\n\nスレッドが終了するのを待っています") c = 0 while Thread.list.length > number_of_thread puts(Thread.list.length) c += 1 if c > timeout then puts('time out') break end sleep(0.1) end endのようにして スレッド数が1つになるまで待つメソッドを用意して
(timeoutは ここでは 0.1秒単位なので デフォルトでは20秒になります)
終了処理中に呼び出すようにします
例えば、exit(0) の所だと
t1.stop() wait_threads_stop() exit(0)ctrl+c をトラップする所だと
Signal.trap(:INT){ t1.stop() wait_threads_stop() exit(0) }こんな感じにすれば
スレッドが強制的にkillされてしまうのを回避できるかもしれません
さて、インスタンシングしたTimerですが
実際にタイマーをスタート、ストップさせるには
t1.start() t1.stop()ですね、上のほうでもstopをサラッと使ってたりしてますね 笑)
あとは・・・、単に インスタンシングしただけではスクリプトの最後まで来ると
否が応でもプログラムが終了してしまいますから
メインルーチンの流れの最後のところで、なんらかのループ処理を書く必要があります
sleep()でそのまま止まり続けますし
メインのループルーチンでも何かしたいのなら
loopやwhileなんかで
loop{ #なんか処理をする sleep(0.1) }のようにするといいですね
では、一通りこれらをまとめたサンプルを書いて終わります
#!/usr/bin/ruby -Ku # ruby 1.8.7 (2012-02-08 patchlevel 358) [i686-linux] # ruby 1.9.3p194 (2012-04-20 revision 35410) [i686-linux] # [UTF-8] [UNIX] class Timer public def initialize(interval, &callback_block) @loop_enable = false @interval = interval @callback_block = callback_block end def start() @loop_enable = true loop_start() end def stop() @loop_enable = false end private def loop_start() Thread.new(){ while @loop_enable @callback_block.call() sleep(@interval) end } end end def wait_threads_stop(number_of_thread = 1,timeout = 200) # デフォルトだと 残り1スレまで待ちます、タイムアウトは20秒 puts("\n\nスレッドが終了するのを待っています") c = 0 while Thread.list.length > number_of_thread puts(Thread.list.length) # 残りスレッド数のデバッグ表示です c += 1 if c > timeout then puts('time out') break end sleep(0.1) end end t1 = Timer.new(0.01){ # インターバルは10msを指定 Thread.new(){ puts('timer 1') sleep(2) # 二秒もかかる処理はないだろうけど、あえて多めに } } t2 = Timer.new(0.05){ Thread.new(){ puts('timer 2') sleep(2) } } t3 = Timer.new(0.1){ puts('timer 3***') sleep(2) } # ctrl+cに対応 Signal.trap(:INT){ t1.stop() t2.stop() t3.stop() wait_threads_stop() exit(0) } t1.start() t2.start() t3.start() sleep(10) t1.stop() t2.stop() t3.stop() wait_threads_stop() exit(0)