確実に一定時間スリープする

Javaで、例えば通信エラー時のリトライ間隔を確保したい場合などのように、一定時間処理を停止したい時はThread.sleep()を使ったりすると思う。

// 三秒間待機する(なんだか問題のある実装!)
public void waitForThreeSeconds() {
    try {
        Thread.sleep(3 * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

Thread.sleepのチェック例外InterruptedExceptionの扱い - torutkのブログ」によると、上のコードは二つの点で問題がある。

  1. 3秒経過する前に割り込みが入りInterruptedExceptionをキャッチした場合、「3秒間待つ」というコントラクトが守られない可能性がある。
  2. InterruptedExceptionをキャッチした場合、InterruptedExceptionを上位に投げ直すか、割り込みステータスを復元して戻る必要がある。(上のように単に握りつぶすと、上位で割り込みステータスを利用できない。)


確実に決められた時間停止するには、InterruptedExceptionをキャッチした後再度残った時間をsleepしなければならない。
また、ループ中にInterruptedExceptionを一度でも受け取った場合、抜ける際に割り込みステータスを設定しなければならない。


リンク先の参照している記事を参考に、それらを盛り込んだコードは例えば次のようになる。

// 確実に三秒間停止する
public void waitForThreeSeconds() {
    long endTime = System.currentTimeMillis() + 3 * 1000; // 3秒後の時間を設定
    boolean interrupted = false;
    try {
        while (true) {
            try {
                long rest = endTime - System.currentTimeMillis();
                if (rest <= 0) {
                    return;
                } else {
                    Thread.sleep(rest);
                }
            } catch (InterruptedException e) {
                interrupted = true; // 一度でも割り込まれたらフラグ設定
            }
        }
    } finally {
        // 一度でも割り込まれていれば、割り込みステータスを設定して終了
        if (interrupted) {
            Thread.currentThread().interrupt();
        }
    }
}

かなり大変なコードになってしまった……。
多分Threadのjoin()とかExecutorServiceのawaitTermination()とかを使う場合でも同じようにする必要がある。
そこで、「最大待ち時間を指定してブロックをおこなう処理でInterruptedExceptionを投げるもの」全般で使えるようにクラス化してみるのはどうだろう。

package sample.interrupt;

public abstract class NoncancelableTask {
    /**
     * endure(long)がtrueを戻すか、指定したmillisを過ぎるまで、残りの時間を引数に指定してendure()を呼び続ける。
     * @param millis ミリ秒。0以下を指定した場合何もせずfalseを戻す。
     * @return endure(long)がtrueを戻した場合true、指定時間が経過した場合falseを戻す。
     */
    public final boolean execute(long millis) {
        if (millis <= 0) {
            return false;
        }
        long endTime = System.currentTimeMillis() + millis;

        long rest = millis;
        boolean interrupted = false;
        try {
            do {
                try {
                    if (endure(rest)) {
                        return true;
                    }
                } catch (InterruptedException e) {
                    interrupted = true;
                }
            } while ((rest = endTime - System.currentTimeMillis()) > 0);
            return false;
        } finally {
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
        }
    }
    /**
     * 一定時間かかり、途中でInterruptedExceptionが呼ばれるような処理を実装する。
     * 処理は、何度でも呼び出し可能である必要がある。
     * @param millis 残りミリ秒。常に1以上。
     * @return 処理を中断するときはtrueを、続行するときはfalseを戻す。
     * @throws InterruptedException 割り込みによる中断があった場合、InterruptedExceptionをそのまま投げる。
     */
    protected abstract boolean endure(long millis) throws InterruptedException;
}

これを継承してendure()を実装すると、残りミリ秒数が引数で渡ってくるので、その時間だけ頑張れ。


例えば、確実3秒間スリープする場合は次のように書く。

new NoncancelableTask() {
    @Override
    protected boolean endure(long millis) throws InterruptedException {
        Thread.sleep(millis);
        return false;
    }
}.execute(3 * 1000L);

他スレッドの終了を最大10秒まで待つ場合は、

final Thread thread = ...//他スレッドが入っている

boolean complete = new NoncancelableTask() {
    @Override
    protected boolean endure(long millis) throws InterruptedException {
        thread.join(millis);
        return !thread.isAlive();
    }
}.execute(10 * 1000L);

ExecutorServiceに登録された全てのジョブが終わるのを最大1時間まで待つ場合は、

final ExecutorService pool = ...//ExecutorServiceが入っている

... ジョブの登録

pool.shutdown();
boolean complete = new NoncancelableTask() {
    @Override
    protected boolean endure(long millis) throws InterruptedException {
        return pool.awaitTermination(millis, TimeUnit.MILLISECONDS);
    }
}.execute(60 * 60 * 1000L);

大分ましな気がする……


永久に待つバージョン作ったりRunnableを実装しとくとさらに便利かも。

追記: 2011/12/5

Threadの割り込みを活用する - プログラマーの脳みそ」のご指摘によると、「「3秒間待つ」というコントラクト」自体がおかしいんじゃないかとのこと。


確かにinterruptを掛ける側のことをあまりに考えなすぎた気もする。他の例外と違って「利用者が意図を持ってinterruptを掛けた場合に」発生するので、利用者の意図に沿わずにブロックし続けるのはあまりフレンドリーでない。


例えばプロジェクトで使うライブラリを作成するとして、多分理想的には最初に実装標準として「Thread#interrupt()を使ってジョブを中断可能にする」か「Thread#interrupt()を使わず終了を待つ」かを決めて、中断可能にする場合はInterruputedException or 割り込みステータスを伝播させることでバックグラウンドのタスク群の全体(または一部)を綺麗に中断させるアーキテクチャを設計するんだと思う。


コードで書くと、

// jobA中で割り込みが掛かるとjobB、jobCは行わずに戻る
public void inBackground() throws InterruputedException {
   jobAWithInterruputedEx();
   jobBWithInterruputedEx();
   jobCWithInterruputedEx();
}

だったり、

// jobA中で割り込みが掛かるとjobB、jobCは行わずに戻る
public void inBackground() {
   jobA();
   if (Thread.currentThread().isInterrupted()) return;
   jobB();
   if (Thread.currentThread().isInterrupted()) return;
   jobC();
}

のような感じ。


逆に「interruptは使いません」という方針の場合には、下のようなコードを書いても、実際に3秒以前にsleepが終わることはない*1ので、

public void waitForThreeSeconds() {
    try {
        Thread.sleep(3 * 1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

どっちにしてもあえてブロックする必要がない、ということかと思う。


まあ「InterruptedExceptionをcatchしてInterruptedExceptionをthrowしない時はThread.currentThread().interrupt()しようね」ってところだけ参考にして下さい。

*1:誰もinterrupt()しないから