1. 概要

この記事では、一定時間後に長時間実行を終了する方法を学習します。 この問題のさまざまな解決策を検討します。 また、彼らの落とし穴のいくつかをカバーします。

2. ループの使用

eコマースアプリケーションの商品アイテムの詳細など、一連のアイテムをループで処理しているが、すべてのアイテムを完了する必要はない場合があると想像してください。

実際、特定の時間までしか処理したくないのですが、その後は実行を停止して、それまでに処理したリストを表示したいと思います。

簡単な例を見てみましょう:

long start = System.currentTimeMillis();
long end = start + 30 * 1000;
while (System.currentTimeMillis() < end) {
    // Some expensive operation on the item.
}

ここで、時間が制限の30秒を超えると、ループが中断されます。 上記のソリューションには、いくつかの注目すべき点があります。

  • 低精度:ループは、課された制限時間より長く実行される可能性があります。 これは、各反復にかかる時間によって異なります。 たとえば、各反復に最大7秒かかる場合、合計時間は最大35秒になる可能性があります。これは、目的の制限時間である30秒よりも約17% lo長い時間です。
  • ブロッキング:メインスレッドでのこのような処理は、長時間ブロックされるため、お勧めできません。 代わりに、これらの操作はメインスレッドから切り離す必要があります

次のセクションでは、割り込みベースのアプローチがこれらの制限をどのように排除するかについて説明します。

3. 割り込みメカニズムの使用

ここでは、別のスレッドを使用して長時間実行される操作を実行します。 メインスレッドは、タイムアウト時にワーカースレッドに割り込みシグナルを送信します。

ワーカースレッドがまだ生きている場合は、シグナルをキャッチして実行を停止します。 ワーカーがタイムアウト前に終了した場合、ワーカースレッドに影響はありません。

ワーカースレッドを見てみましょう。

class LongRunningTask implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < Long.MAX_VALUE; i++) {
            if(Thread.interrupted()) {
                return;
            }
        }
    }
}

ここで、 Long.MAX_VALUE を通るforループは、長時間実行される操作をシミュレートします。 これの代わりに、他の操作が存在する可能性があります。 すべての操作が割り込み可能であるとは限らないため、割り込みフラグをチェックすることが重要です。 したがって、そのような場合は、手動でフラグを確認する必要があります。

また、すべての反復でこのフラグをチェックして、スレッドが最大で1回の反復の遅延内に実行を停止することを確認する必要があります。

次に、割り込み信号を送信する3つの異なるメカニズムについて説明します。

3.1. タイマーを使用する

または、 TimerTask を作成して、タイムアウト時にワーカースレッドに割り込むこともできます。

class TimeOutTask extends TimerTask {
    private Thread thread;
    private Timer timer;

    public TimeOutTask(Thread thread, Timer timer) {
        this.thread = thread;
        this.timer = timer;
    }

    @Override
    public void run() {
        if(thread != null && thread.isAlive()) {
            thread.interrupt();
            timer.cancel();
        }
    }
}

ここでは、作成時にワーカースレッドを取得するTimerTaskを定義しました。 runメソッドの呼び出し時にワーカースレッドを中断します Timer は、3秒の遅延後にTimerTaskをトリガーします。

Thread thread = new Thread(new LongRunningTask());
thread.start();

Timer timer = new Timer();
TimeOutTask timeOutTask = new TimeOutTask(thread, timer);
timer.schedule(timeOutTask, 3000);

3.2. メソッドの使用Future#get

Timer を使用する代わりに、Futuregetメソッドを使用することもできます。

ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(new LongRunningTask());
try {
    future.get(7, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    future.cancel(true);
} catch (Exception e) {
    // handle other exceptions
} finally {
    executor.shutdownNow();
}

ここでは、 ExecutorService を使用して、 Futureのインスタンスを返すワーカースレッドを送信しました。このインスタンスのgetメソッドは、指定された時間までメインスレッドをブロックします。 指定されたタイムアウト後にTimeoutExceptionが発生します。 catch ブロックでは、 F utureオブジェクトでcancelメソッドを呼び出すことにより、ワーカースレッドを中断しています。

前のアプローチに対するこのアプローチの主な利点は、がプールを使用してスレッドを管理するのに対し、Timerは単一のスレッドのみ(プールなし)を使用することです。

3.3. ScheduledExcecutorSercviceを使用する

ScheduledExecutorServiceを使用してタスクを中断することもできます。 このクラスはExecutorServiceの拡張であり、実行のスケジューリングを処理するいくつかのメソッドが追加された同じ機能を提供します。 これにより、設定された時間単位の特定の遅延の後に、指定されたタスクを実行できます。

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
Future future = executor.submit(new LongRunningTask());
Runnable cancelTask = () -> future.cancel(true);

executor.schedule(cancelTask, 3000, TimeUnit.MILLISECONDS);
executor.shutdown();

ここでは、メソッドnewScheduledThreadPoolを使用してサイズ2のスケジュールされたスレッドプールを作成しました。 ScheduledExecutorService# schedule メソッドは、 Runnable 、遅延値、および遅延の単位を取ります。

上記のプログラムは、送信時から3秒後にタスクを実行するようにスケジュールします。 このタスクは、元の長時間実行タスクをキャンセルします。

前のアプローチとは異なり、 Future#getメソッドを呼び出してメインスレッドをブロックしていないことに注意してください。 したがって、これは上記のすべてのアプローチの中で最も好ましいアプローチです。

4. 保証はありますか?

一定時間後に実行が停止する保証はありません。 主な理由は、すべてのブロッキングメソッドが割り込み可能であるとは限らないことです。 実際、割り込み可能な明確に定義されたメソッドはごくわずかです。 したがって、スレッドが中断され、フラグが設定された場合、これらの中断可能なメソッドのいずれかに到達するまで他に何も起こりません。

たとえば、readおよびwriteメソッドは、InterruptibleChannelで作成されたストリームで呼び出された場合にのみ割り込み可能です。 BufferedReaderInterruptibleChannelではありません。 したがって、スレッドがそれを使用してファイルを読み取る場合、 readメソッドでブロックされたこのスレッドでinterrupt()を呼び出しても効果はありません。

ただし、ループで読み取るたびに、割り込みフラグを明示的にチェックできます。 これにより、ある程度の遅延でスレッドを停止するための妥当な確実性が得られます。 ただし、読み取り操作にかかる時間がわからないため、厳密な時間の後にスレッドを停止することは保証されません。

一方、Objectクラスのwaitメソッドは割り込み可能です。 したがって、 wait メソッドでブロックされたスレッドは、割り込みフラグが設定された後、すぐにInterruptedExceptionをスローします。

メソッドシグネチャでthrows InterruptedException を探すことで、ブロッキングメソッドを特定できます。

重要なアドバイスの1つは、 非推奨のThread.stop()メソッドの使用は避けてください。 スレッドを停止すると、ロックされているすべてのモニターのロックが解除されます。 これは、スタックを伝播するThreadDeath例外が原因で発生します。

これらのモニターによって以前に保護されていたオブジェクトのいずれかが不整合な状態にあった場合、不整合なオブジェクトは他のスレッドに表示されます。 これは、検出や推論が非常に難しい任意の動作につながる可能性があります。

5. 中断のための設計

前のセクションでは、実行をできるだけ早く停止するための割り込み可能なメソッドを用意することの重要性を強調しました。 したがって、私たちのコードは、設計の観点からこの期待を考慮する必要があります。

実行するタスクが長時間実行されていると想像してください。指定された時間よりも時間がかからないようにする必要があります。 また、タスクを個々のステップに分割できるとします。

タスクステップのクラスを作成しましょう。

class Step {
    private static int MAX = Integer.MAX_VALUE/2;
    int number;

    public Step(int number) {
        this.number = number;
    }

    public void perform() throws InterruptedException {
        Random rnd = new Random();
        int target = rnd.nextInt(MAX);
        while (rnd.nextInt(MAX) != target) {
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
        }
    }
}

ここで、 Step#perform メソッドは、各反復でフラグを要求しながら、ターゲットのランダムな整数を見つけようとします。 フラグがアクティブ化されると、メソッドはInterruptedExceptionをスローします。

次に、すべてのステップを実行するタスクを定義しましょう。

public class SteppedTask implements Runnable {
    private List<Step> steps;

    public SteppedTask(List<Step> steps) {
        this.steps = steps;
    }

    @Override
    public void run() {
        for (Step step : steps) {
            try {
                step.perform();
            } catch (InterruptedException e) {
                // handle interruption exception
                return;
            }
        }
    }
}

ここで、SteppedTaskには実行するステップのリストがあります。 forループは各ステップを実行し、タスクが発生したときにタスクを停止するためにInterruptedExceptionを処理します。

最後に、割り込み可能なタスクの使用例を見てみましょう。

List<Step> steps = Stream.of(
  new Step(1),
  new Step(2),
  new Step(3),
  new Step(4))
.collect(Collectors.toList());

Thread thread = new Thread(new SteppedTask(steps));
thread.start();

Timer timer = new Timer();
TimeOutTask timeOutTask = new TimeOutTask(thread, timer);
timer.schedule(timeOutTask, 10000);

まず、4つのステップでSteppedTaskを作成します。 次に、スレッドを使用してタスクを実行します。 最後に、タイマーとタイムアウトタスクを使用して、10秒後にスレッドを中断します。

この設計により、任意のステップの実行中に長時間実行されるタスクを確実に中断できます。 前に見たように、欠点は、指定された正確な時間に停止するという保証はありませんが、中断できないタスクよりも確かに優れていることです。

6. 結論

このチュートリアルでは、それぞれの長所と短所とともに、一定時間後に実行を停止するためのさまざまな手法を学びました。 完全なソースコードは、GitHubにあります。