確実に一定時間スリープする
Javaで、例えば通信エラー時のリトライ間隔を確保したい場合などのように、一定時間処理を停止したい時はThread.sleep()を使ったりすると思う。
// 三秒間待機する(なんだか問題のある実装!) public void waitForThreeSeconds() { try { Thread.sleep(3 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } }
「Thread.sleepのチェック例外InterruptedExceptionの扱い - torutkのブログ」によると、上のコードは二つの点で問題がある。
- 3秒経過する前に割り込みが入りInterruptedExceptionをキャッチした場合、「3秒間待つ」というコントラクトが守られない可能性がある。
- 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()しないから