タイムアウト後にタスクを中断するExecutorService


93

タイムアウトを指定できるExecutorService実装を探しています。ExecutorServiceに送信されたタスクは、実行にタイムアウトよりも長い時間がかかると中断されます。そのような獣を実装することはそれほど難しい作業ではありませんが、誰かが既存の実装を知っているかどうか疑問に思っています。

以下は、以下の議論に基づいて私が思いついたものです。コメントは?

import java.util.List;
import java.util.concurrent.*;

public class TimeoutThreadPoolExecutor extends ThreadPoolExecutor {
    private final long timeout;
    private final TimeUnit timeoutUnit;

    private final ScheduledExecutorService timeoutExecutor = Executors.newSingleThreadScheduledExecutor();
    private final ConcurrentMap<Runnable, ScheduledFuture> runningTasks = new ConcurrentHashMap<Runnable, ScheduledFuture>();

    public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, long timeout, TimeUnit timeoutUnit) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        this.timeout = timeout;
        this.timeoutUnit = timeoutUnit;
    }

    public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, long timeout, TimeUnit timeoutUnit) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
        this.timeout = timeout;
        this.timeoutUnit = timeoutUnit;
    }

    public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler, long timeout, TimeUnit timeoutUnit) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
        this.timeout = timeout;
        this.timeoutUnit = timeoutUnit;
    }

    public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler, long timeout, TimeUnit timeoutUnit) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
        this.timeout = timeout;
        this.timeoutUnit = timeoutUnit;
    }

    @Override
    public void shutdown() {
        timeoutExecutor.shutdown();
        super.shutdown();
    }

    @Override
    public List<Runnable> shutdownNow() {
        timeoutExecutor.shutdownNow();
        return super.shutdownNow();
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        if(timeout > 0) {
            final ScheduledFuture<?> scheduled = timeoutExecutor.schedule(new TimeoutTask(t), timeout, timeoutUnit);
            runningTasks.put(r, scheduled);
        }
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        ScheduledFuture timeoutTask = runningTasks.remove(r);
        if(timeoutTask != null) {
            timeoutTask.cancel(false);
        }
    }

    class TimeoutTask implements Runnable {
        private final Thread thread;

        public TimeoutTask(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            thread.interrupt();
        }
    }
}

タイムアウトの「開始時間」は提出の時間ですか?または、タスクが実行を開始する時間は?
Tim Bender、

良い質問。実行を開始したとき。おそらくprotected void beforeExecute(Thread t, Runnable r)フックを使用しています。
エドワードデール

@ scompt.comあなたはまだ、このソリューションを使用しているか、それが取って代わられました
ポール・テイラー

@PaulTaylorこのソリューションを実装した仕事は廃止されました。:-)
Edward Dale

私はこれを正確に必要としますが、a)メインスケジューラサービスを単一のサービススレッドのスレッドプールにする必要があります。タスクを厳密に同時に実行する必要があるためです。b)で各タスクのタイムアウト期間を指定できる必要があります。タスクが送信された時間。これを出発点として使用してみましたが、ScheduledThreadPoolExecutorを拡張しましたが、タスクの実行時にbeforeExecuteメソッドに指定される、指定されたタイムアウト期間を取得する方法がわかりません。どんな提案もありがたいです!
マイケル・エリス

回答:


89

これには、ScheduledExecutorServiceを使用できます。まず、それを一度だけ送信してすぐに開始し、作成された未来を保持します。その後、一定期間後に保留された将来をキャンセルする新しいタスクを送信できます。

 ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); 
 final Future handler = executor.submit(new Callable(){ ... });
 executor.schedule(new Runnable(){
     public void run(){
         handler.cancel();
     }      
 }, 10000, TimeUnit.MILLISECONDS);

これにより、ハンドラー(中断される主な機能)が10秒間実行され、その特定のタスクがキャンセル(つまり、中断)されます。


12
興味深いアイデアですが、タイムアウト前にタスクが完了するとどうなりますか(通常はタイムアウトになります)。割り当てられたタスクが既に完了していることを確認するためだけに実行を待機している大量のクリーンアップタスクは必要ありません。クリーンアップタスクを削除するために終了すると、Futureを監視する別のスレッドが必要になります。
Edward Dale、

3
エグゼキュータは、このキャンセルを一度だけスケジュールします。タスクが完了した場合、キャンセルはノーオペレーションであり、作業は変更されずに続行されます。タスクをキャンセルするための1つの追加のスレッドスケジュールと、それらを実行するための1つのスレッドのみが必要です。2つのエグゼキューターがあり、1つはメインタスクを送信し、もう1つはそれらをキャンセルします。
ジョンヴィント

3
それは事実ですが、タイムアウトが5時間で、その間に10kのタスクが実行された場合はどうでしょう。私は、メモリを消費してコンテキストスイッチを引き起こして、何もしないことを回避したいと思います。
エドワードデール

1
@Scompt必ずしもそうとは限りません。10,000のfuture.cancel()呼び出しがありますが、futureが完了した場合、キャンセルは高速パスアウトし、不必要な作業を行いません。10kの追加のキャンセル呼び出しが必要ない場合、これは機能しない可能性がありますが、タスクが完了したときに行われる作業の量はごくわずかです。
ジョンヴィント

6
@ジョンW:私はあなたの実装に別の問題を認識しました。以前にコメントしたように、タスクが実行を開始したときにタイムアウトを開始する必要があります。それを行う唯一の方法は、beforeExecuteフックを使用することだと思います。
Edward Dale

6

残念ながら、このソリューションには欠陥があります。にも一種のバグがありScheduledThreadPoolExecutorこの質問でも報告されています。送信されたタスクをキャンセルしても、タスクに関連付けられたメモリリソースは完全には解放されません。リソースは、タスクの有効期限が切れたときにのみ解放されます。

したがってTimeoutThreadPoolExecutor、かなり長い有効期限(通常の使用法)でを作成し、タスクを十分に速く送信すると、タスクが実際に正常に完了したとしても、メモリがいっぱいになります。

次の(非常に大まかな)テストプログラムで問題を確認できます。

public static void main(String[] args) throws InterruptedException {
    ExecutorService service = new TimeoutThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, 
            new LinkedBlockingQueue<Runnable>(), 10, TimeUnit.MINUTES);
    //ExecutorService service = Executors.newFixedThreadPool(1);
    try {
        final AtomicInteger counter = new AtomicInteger();
        for (long i = 0; i < 10000000; i++) {
            service.submit(new Runnable() {
                @Override
                public void run() {
                    counter.incrementAndGet();
                }
            });
            if (i % 10000 == 0) {
                System.out.println(i + "/" + counter.get());
                while (i > counter.get()) {
                    Thread.sleep(10);
                }
            }
        }
    } finally {
        service.shutdown();
    }
}

プログラムは、生成されたRunnablesが完了するまで待機しますが、利用可能なメモリを使い果たします。

私はしばらくこれについて考えましたが、残念ながら私は良い解決策を思い付くことができませんでした。

編集:この問題はJDKバグ6602600として報告されており、ごく最近修正されたようです。


4

タスクをFutureTaskでラップし、FutureTaskのタイムアウトを指定できます。この質問に対する私の回答の例を見てください。

Javaネイティブプロセスタイムアウト


1
java.util.concurrentクラスを使用してこれを行うにはいくつかの方法があることを理解していますが、ExecutorService実装を探しています。
Edward Dale、

1
ExecutorServiceにクライアントコードからタイムアウトが追加されているという事実を隠したい場合は、実行する前に、FutureTaskで渡されたすべての実行可能ファイルをラップする独自のExecutorServiceを実装できます。
erikprice 2010

2

調査するための
膨大な時間の後、最後に、私はこの問題を解決するinvokeAll方法を使用しますExecutorService
これにより、タスクの実行中にタスクが厳密に中断されます。
ここに例があります

ExecutorService executorService = Executors.newCachedThreadPool();

try {
    List<Callable<Object>> callables = new ArrayList<>();
    // Add your long time task (callable)
    callables.add(new VaryLongTimeTask());
    // Assign tasks for specific execution timeout (e.g. 2 sec)
    List<Future<Object>> futures = executorService.invokeAll(callables, 2000, TimeUnit.MILLISECONDS);
    for (Future<Object> future : futures) {
        // Getting result
    }
} catch (InterruptedException e) {
    e.printStackTrace();
}

executorService.shutdown();

プロでもListenableFuture同じで提出できますExecutorService
コードの最初の行を少しだけ変更します。

ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());

ListeningExecutorServiceExecutorServiceグアバグアバプロジェクトのリスニング機能です(com.google.guava))


2
ご指摘ありがとうございinvokeAllます。それはとてもうまくいきます。これを使用することを考えている人への警告の言葉:オブジェクトのinvokeAllリストを返しますが、Future実際にはブロック操作のようです。
mxro 2016年


1

問題はJDKバグ6602600(2010-05-22で解決済み)ではなく、sleep(10)が正しく呼び出されていないことが原因のようです。さらに、メインスレッドは、外側の円のすべてのブランチでSLEEP(0)を呼び出してタスクを実現するために、他のスレッドに直接CHANCEを与える必要があることに注意してください。Thread.sleep(0)の代わりにThread.yield()を使用する方が良いと思います

以前の問題コードの結果を修正した部分は次のようになります。

.......................
........................
Thread.yield();         

if (i % 1000== 0) {
System.out.println(i + "/" + counter.get()+ "/"+service.toString());
}

//                
//                while (i > counter.get()) {
//                    Thread.sleep(10);
//                } 

これは、最大150 000 000のテスト済みサークルまでの外部カウンターの量で正しく機能します。


1

John W Answerを使用して、タスクが実行を開始したときにタイムアウトを正しく開始する実装を作成しました。私はそれのためのユニットテストも書いています:)

ただし、一部のIO操作はFuture.cancel()が呼び出されたときに(つまり、Thread.interrupt()が呼び出されたときに)中断されないため、私のニーズには適していません。Thread.interrupt()が呼び出されても中断されない可能性のあるIO操作の例としてはSocket.connect、and がありますSocket.read(およびで実装されているIO操作のほとんどが疑われますjava.io)。が呼び出されると、のすべてのIO操作はjava.nio割り込み可能でなければなりませんThread.interrupt()。たとえば、SocketChannel.openおよびの場合ですSocketChannel.read

とにかく、誰かが興味を持っている場合は、タスクがタイムアウトになるようにするスレッドプールエグゼキューターの要旨を作成しました(割り込み可能な操作を使用している場合...):https : //gist.github.com/amanteaux/64c54a913c1ae34ad7b86db109cbc0bf


興味深いコードですが、システムに組み込んだので、どのようなIO操作が中断しないかの例があるので、システムに影響があるかどうかを確認できます。ありがとう!
ダンカンクレブス

:私は非割り込みIOの例で私の答えを詳細に@DuncanKrebs Socket.connectSocket.read
amanteaux

myThread.interrupted()中断フラグをクリアするため、中断する正しい方法ではありません。myThread.interrupt()代わりに使用してください。これはソケットで必要です
DanielCuadra

@DanielCuadra:ありがとうございますThread.interrupted()。スレッドを中断できないため、タイプミスを犯したようです。ただし、操作をThread.interrupt()中断せず、java.io操作に対してのみ機能しjava.nioます。
アマントー

私はinterrupt()長年使用しており、java.io操作(およびスレッドスリープ、jdbc接続、blockingqueue takeなどの他のブロッキングメソッド)を常に中断してきました。バグのあるクラスまたはバグのあるJVMを見つけたのかもしれません
DanielCuadra

0

この代替案はどうですか?

  • 2つには2つのエグゼキューターがあります。
    • 1つ:
      • タスクのタイムアウトを気にせずにタスクを送信する
      • 結果として生じる未来とそれが内部構造に終わるべき時を追加する
    • 1つは、一部のタスクがタイムアウトした場合や、それらをキャンセルする必要がある場合に、内部構造をチェックする内部ジョブを実行するためのものです。

小さなサンプルはこちら:

public class AlternativeExecutorService 
{

private final CopyOnWriteArrayList<ListenableFutureTask> futureQueue       = new CopyOnWriteArrayList();
private final ScheduledThreadPoolExecutor                scheduledExecutor = new ScheduledThreadPoolExecutor(1); // used for internal cleaning job
private final ListeningExecutorService                   threadExecutor    = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(5)); // used for
private ScheduledFuture scheduledFuture;
private static final long INTERNAL_JOB_CLEANUP_FREQUENCY = 1000L;

public AlternativeExecutorService()
{
    scheduledFuture = scheduledExecutor.scheduleAtFixedRate(new TimeoutManagerJob(), 0, INTERNAL_JOB_CLEANUP_FREQUENCY, TimeUnit.MILLISECONDS);
}

public void pushTask(OwnTask task)
{
    ListenableFuture<Void> future = threadExecutor.submit(task);  // -> create your Callable
    futureQueue.add(new ListenableFutureTask(future, task, getCurrentMillisecondsTime())); // -> store the time when the task should end
}

public void shutdownInternalScheduledExecutor()
{
    scheduledFuture.cancel(true);
    scheduledExecutor.shutdownNow();
}

long getCurrentMillisecondsTime()
{
    return Calendar.getInstance().get(Calendar.MILLISECOND);
}

class ListenableFutureTask
{
    private final ListenableFuture<Void> future;
    private final OwnTask                task;
    private final long                   milliSecEndTime;

    private ListenableFutureTask(ListenableFuture<Void> future, OwnTask task, long milliSecStartTime)
    {
        this.future = future;
        this.task = task;
        this.milliSecEndTime = milliSecStartTime + task.getTimeUnit().convert(task.getTimeoutDuration(), TimeUnit.MILLISECONDS);
    }

    ListenableFuture<Void> getFuture()
    {
        return future;
    }

    OwnTask getTask()
    {
        return task;
    }

    long getMilliSecEndTime()
    {
        return milliSecEndTime;
    }
}

class TimeoutManagerJob implements Runnable
{
    CopyOnWriteArrayList<ListenableFutureTask> getCopyOnWriteArrayList()
    {
        return futureQueue;
    }

    @Override
    public void run()
    {
        long currentMileSecValue = getCurrentMillisecondsTime();
        for (ListenableFutureTask futureTask : futureQueue)
        {
            consumeFuture(futureTask, currentMileSecValue);
        }
    }

    private void consumeFuture(ListenableFutureTask futureTask, long currentMileSecValue)
    {
        ListenableFuture<Void> future = futureTask.getFuture();
        boolean isTimeout = futureTask.getMilliSecEndTime() >= currentMileSecValue;
        if (isTimeout)
        {
            if (!future.isDone())
            {
                future.cancel(true);
            }
            futureQueue.remove(futureTask);
        }
    }
}

class OwnTask implements Callable<Void>
{
    private long     timeoutDuration;
    private TimeUnit timeUnit;

    OwnTask(long timeoutDuration, TimeUnit timeUnit)
    {
        this.timeoutDuration = timeoutDuration;
        this.timeUnit = timeUnit;
    }

    @Override
    public Void call() throws Exception
    {
        // do logic
        return null;
    }

    public long getTimeoutDuration()
    {
        return timeoutDuration;
    }

    public TimeUnit getTimeUnit()
    {
        return timeUnit;
    }
}
}

0

これがうまくいくか確認してください

    public <T,S,K,V> ResponseObject<Collection<ResponseObject<T>>> runOnScheduler(ThreadPoolExecutor threadPoolExecutor,
      int parallelismLevel, TimeUnit timeUnit, int timeToCompleteEachTask, Collection<S> collection,
      Map<K,V> context, Task<T,S,K,V> someTask){
    if(threadPoolExecutor==null){
      return ResponseObject.<Collection<ResponseObject<T>>>builder().errorCode("500").errorMessage("threadPoolExecutor can not be null").build();
    }
    if(someTask==null){
      return ResponseObject.<Collection<ResponseObject<T>>>builder().errorCode("500").errorMessage("Task can not be null").build();
    }
    if(CollectionUtils.isEmpty(collection)){
      return ResponseObject.<Collection<ResponseObject<T>>>builder().errorCode("500").errorMessage("input collection can not be empty").build();
    }

    LinkedBlockingQueue<Callable<T>> callableLinkedBlockingQueue = new LinkedBlockingQueue<>(collection.size());
    collection.forEach(value -> {
      callableLinkedBlockingQueue.offer(()->someTask.perform(value,context)); //pass some values in callable. which can be anything.
    });
    LinkedBlockingQueue<Future<T>> futures = new LinkedBlockingQueue<>();

    int count = 0;

    while(count<parallelismLevel && count < callableLinkedBlockingQueue.size()){
      Future<T> f = threadPoolExecutor.submit(callableLinkedBlockingQueue.poll());
      futures.offer(f);
      count++;
    }

    Collection<ResponseObject<T>> responseCollection = new ArrayList<>();

    while(futures.size()>0){
      Future<T> future = futures.poll();
      ResponseObject<T> responseObject = null;
        try {
          T response = future.get(timeToCompleteEachTask, timeUnit);
          responseObject = ResponseObject.<T>builder().data(response).build();
        } catch (InterruptedException e) {
          future.cancel(true);
        } catch (ExecutionException e) {
          future.cancel(true);
        } catch (TimeoutException e) {
          future.cancel(true);
        } finally {
          if (Objects.nonNull(responseObject)) {
            responseCollection.add(responseObject);
          }
          futures.remove(future);//remove this
          Callable<T> callable = getRemainingCallables(callableLinkedBlockingQueue);
          if(null!=callable){
            Future<T> f = threadPoolExecutor.submit(callable);
            futures.add(f);
          }
        }

    }
    return ResponseObject.<Collection<ResponseObject<T>>>builder().data(responseCollection).build();
  }

  private <T> Callable<T> getRemainingCallables(LinkedBlockingQueue<Callable<T>> callableLinkedBlockingQueue){
    if(callableLinkedBlockingQueue.size()>0){
      return callableLinkedBlockingQueue.poll();
    }
    return null;
  }

スケジューラからのスレッドの使用を制限したり、タスクにタイムアウトを設定したりできます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.