Thursday, April 23, 2020 / Java, Groovy, Programming

java.util.Timer を使って、連続で発生するイベントが小休止のときを見計らって処理する

手書きのタブレット向けアプリで、ペンの入力が連続して発生するのだが、そのたびに保存処理を行うと作動が重くなる。 そこで、ちょっと入力が止んだそのスキを見計らって、保存処理を実行するようにしたい。 そのための java.util.Timer と TimerTask の使い方のメモ。
例によってコードはすべて Groovy です。

0.5 秒毎にイベントを発生させるコード

// 0.5秒おきに 5件 のイベントを発生させる.
int cnt = 5
while( cnt>0 ){
    cnt = cnt-1 // count down
    println "- ${cnt}"
    Thread.sleep(500)
}

小休止(1.5秒)を挟んで 0.5秒毎にイベントを発生

def process = {
    int cnt = 5
    while( cnt>0 ){
        cnt = cnt-1 // count down
        println "- ${cnt}"
        Thread.sleep(500)
    }
}

// 0.5秒おきに 5件 のイベントを発生させる.
process()

// 1.5秒休む
Thread.sleep(1500)

// 0.5秒おきに 5件 のイベントを発生させる.
process()

小休止時に処理をする

問題は、小休止をどうやって判定するか。 いろいろな方法がありそうですが、ここでは、イベントが起きたら1秒後に実行するTimerTask を起動する方法を使います。 ロジックは以下の通り:

    1. イベント発生時に TimerTask を生成して 1秒後に処理が起きるようにセット
    1. 次のイベントきたら、直前の TimerTask インスタンスを cancel する
    • つまりそのイベントより1秒以内で処理を起こすつもりの TimerTask を cancel することになる
    • 逆に言えばそのイベントと前のイベントの間の時間が 1秒以上あれば、 TimerTask は cancel されずに実行される

これにより 1秒より長い間隔の小休止が発生したら処理をする を実現してみます。

小休止処理 Step 1

最初のステップとして、とにかくイベント起きたら、TimerTask を生成して 1秒後に処理を実行する、を実装:

import java.util.Timer
import java.util.TimerTask

class MyTimerTask extends TimerTask {
    int myCnt
    void run(){
        println "-- run timerTask ${myCnt}"
    }
}

def process = { timer->
    int cnt = 5
    while( cnt>0 ){
        cnt = cnt-1 // count down
        println "- create timerTask ${cnt}"
        def timerTask = new MyTimerTask([myCnt: cnt])
        timer.schedule(timerTask, 1000) // 1秒後に起動するようにスケジュール.
        Thread.sleep(500)
    }
}

// timer 作成
def timer = new Timer(false)

// 0.5秒おきに 5件 のイベントを発生させる.
process(timer)

// 1.5秒休む
Thread.sleep(1500)

// 0.5秒おきに 5件 のイベントを発生させる.
process(timer)

実行すると以下のようになります:

- create timerTask 4
- create timerTask 3
-- run timerTask 4
- create timerTask 2
-- run timerTask 3
- create timerTask 1
-- run timerTask 2
- create timerTask 0
-- run timerTask 1
-- run timerTask 0
- create timerTask 4
- create timerTask 3
-- run timerTask 4
- create timerTask 2
-- run timerTask 3
- create timerTask 1
-- run timerTask 2
- create timerTask 0
-- run timerTask 1
-- run timerTask 0

まだ 直前の TimerTask の cancel 処理を入れていないので、すべての timerTask が 1秒遅れで 実行されています。

小休止処理 Step 2

直前の TimerTask インスタンスをキャンセルすることで、1秒以上の間隔があるときだけ TimerTask が実行されるようにします。
(process 部分だけの抜粋)

def prevTimerTask = null

def process = { timer->
    int cnt = 5
    while( cnt>0 ){
		// do cancel
        prevTimerTask?.cancel()

        cnt = cnt-1 // count down
        println "- create timerTask ${cnt}"
        def timerTask = new MyTimerTask([myCnt: cnt])
        timer.schedule(timerTask, 1000) // 1秒後に起動するようにスケジュール.

		// keep it
        prevTimerTask = timerTask

        Thread.sleep(500)
    }
}

実行すると以下のようになります:

- create timerTask 4
- create timerTask 3
- create timerTask 2
- create timerTask 1
- create timerTask 0
-- run timerTask 0
- create timerTask 4
- create timerTask 3
- create timerTask 2
- create timerTask 1
- create timerTask 0
-- run timerTask 0

これで一応意図通りの作動になりました。

プログラムが終了しない問題

ただ、これ処理が終わってもプログラムが終了しないのが気持ち悪い。 Timer インスタンスが残っている状態なので、タブレットアプリなどではメモリリークするのかもしれません。

new Timer(false)

により、デーモンスレッドにするか、ユーザースレッドにするかの指定を行う。ここでは、false を指定してユーザースレッドにしている。
この場合、プログラムが終了しなくなる。

そこで:

new Timer(true)

としてデーモンスレッドにすると、プログラムは終了する。

しかし、最後の TimerTask 実行前にプログラムが終了するので、最後の TimerTask の処理は実行されない。 これはまずい。
もし今想定しているような、手書きアプリでの入力保存に使う場合、最後の入力データが保存されないことになる。
どうすりゃいいの?

あと、Timerインスタンスの生成コストは低いのかな?1秒おきに Timer インスタンスを生成するのだから、もし重い処理だったら本末転倒になる。