Java:特定のキューサイズの後に送信をブロックするExecutorService


85

単一のスレッドが並行して実行できるI / O集約型タスクを生成するソリューションをコーディングしようとしています。各タスクには、重要なメモリ内データがあります。そのため、現在保留中のタスクの数を制限できるようにしたいと思います。

このようにThreadPoolExecutorを作成すると、次のようになります。

    ThreadPoolExecutor executor = new ThreadPoolExecutor(numWorkerThreads, numWorkerThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(maxQueue));

次に、キューがいっぱいになり、すべてのスレッドがすでにビジー状態になるとexecutor.submit(callable)スローさRejectedExecutionExceptionれます。

executor.submit(callable)キューがいっぱいですべてのスレッドがビジーのときにブロックを作成するにはどうすればよいですか?

編集:私はこれを試しました

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

そして、それは私が達成したい効果をいくらか達成しますが、エレガントではありません(基本的に拒否されたスレッドは呼び出しスレッドで実行されるため、呼び出しスレッドがそれ以上送信するのをブロックします)。

編集:(質問をしてから5年後)

この質問とその回答を読んでいる人には、受け入れられた回答を1つの正しい解決策と見なさないでください。すべての回答とコメントをお読みください。



1
@axtavtにリンクされている非常によく似た質問への回答と同じように、私は以前にセマフォを使用してそれを正確に実行しました。
Stephen Denne 2010

2
@TomWolk 1つにはnumWorkerThreads、呼び出し元のスレッドもタスクを実行している場合よりも、並行して実行されるタスクが1つ多くなります。ただし、より重要な問題は、呼び出し元のスレッドが長時間実行されるタスクを取得した場合、他のスレッドがアイドル状態で次のタスクを待機する可能性があることです。
Tahir Akhtar 2016年

2
@TahirAkhtar、本当。キューは、呼び出し元が自分でタスクを実行する必要があるときに枯渇しないように、十分に長くする必要があります。しかし、もう1つのスレッドである呼び出し元スレッドを使用してタスクを実行できると便利だと思います。発信者がブロックするだけの場合、発信者のスレッドはアイドル状態になります。スレッドプールの容量の3倍のキューでCallerRunsPolicyを使用すると、スムーズに機能します。このソリューションと比較して、フレームワークのオーバーエンジニアリングによる焼き戻しを検討します。
TomWolk 2016年

1
@ TomWalk + 1良い点。別の違いのように思われるのは、タスクがキューから拒否され、呼び出し元スレッドによって実行された場合、呼び出し元スレッドはキュー内で順番を待たなかったため、要求の処理を順不同で開始することです。確かに、スレッドの使用をすでに選択している場合は、依存関係を適切に処理する必要がありますが、覚えておくべきことがあります。
rimsky 2016年

回答:


64

私はこれと同じことをしました。秘訣は、offer()メソッドが実際にはput()であるBlockingQueueを作成することです。(任意の基本BlockingQueue implを使用できます)。

public class LimitedQueue<E> extends LinkedBlockingQueue<E> 
{
    public LimitedQueue(int maxSize)
    {
        super(maxSize);
    }

    @Override
    public boolean offer(E e)
    {
        // turn offer() and add() into a blocking calls (unless interrupted)
        try {
            put(e);
            return true;
        } catch(InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
        return false;
    }

}

これはスレッドプールでのみ機能corePoolSize==maxPoolSizeすることに注意してください。そこでは注意が必要です(コメントを参照)。


2
または、SynchronousQueueを拡張してバッファリングを防ぎ、直接ハンドオフのみを許可することもできます。
brendon 2014年

エレガントで問題に直接対処します。オファー()はプット()になり、プット()は「...必要に応じてスペースが利用可能になるのを待つ」という意味です
トレントン2015年

5
オファー方法のプロトコルが変わるので、これは良い考えではないと思います。オファーメソッドはノンブロッキングコールである必要があります。
明江市2015

6
同意しません-これにより、ThreadPoolExecutor.executeの動作が変更され、corePoolSize <maxPoolSizeの場合、ThreadPoolExecutorロジックがコアを超えてワーカーを追加することはありません。
krease

5
明確にするために-ソリューションは、どこで制約を維持している場合にのみ機能しますcorePoolSize==maxPoolSize。これがないと、ThreadPoolExecutorに設計された動作をさせることができなくなります。私は、その制限がなかったこの問題の解決策を探していました。最終的に採用したアプローチについては、以下の私の代替回答を参照してください。
krease 2015

15

これが私の側でこれをどのように解決したかです:

(注:このソリューションは、Callableを送信するスレッドをブロックするため、RejectedExecutionExceptionがスローされるのを防ぎます)

public class BoundedExecutor extends ThreadPoolExecutor{

    private final Semaphore semaphore;

    public BoundedExecutor(int bound) {
        super(bound, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        semaphore = new Semaphore(bound);
    }

    /**Submits task to execution pool, but blocks while number of running threads 
     * has reached the bound limit
     */
    public <T> Future<T> submitButBlockIfFull(final Callable<T> task) throws InterruptedException{

        semaphore.acquire();            
        return submit(task);                    
    }


    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);

        semaphore.release();
    }
}

1
これはcorePoolSize < maxPoolSize...:|の場合にはうまく機能しないと思います
rogerdpack 2017

1
の場合に動作しcorePoolSize < maxPoolSizeます。そのような場合、セマフォは使用可能になりますが、スレッドは存在せず、SynchronousQueueはfalseを返します。ThreadPoolExecutorその後、新しいスレッドをスピンします。このソリューションの問題は、競合状態があることです。後semaphore.release()、ただしスレッドが終了する前にexecute、submit()はセマフォ許可を取得します。IF super.submitは()の前に実行されてexecute()終了すると、ジョブは拒否されます。
ルイス・ギリェルメ

@LuísGuilhermeただし、スレッドが実行を終了する前にsemaphore.release()が呼び出されることはありません。このコールがで行われるため、後に実行(...)メソッド。あなたが説明しているシナリオで何かが欠けていますか?
cvacca 2018年

1
afterExecuteは、タスクを実行するのと同じスレッドによって呼び出されるため、まだ終了していません。自分でテストしてください。そのソリューションを実装し、エグゼキュータに大量の作業をスローし、作業が拒否された場合はスローします。はい、これには競合状態があり、再現するのは難しくありません。
ルイス・ギリェルメ

1
ThreadPoolExecutorに移動し、runWorker(Worker w)メソッドを確認します。ワーカーのロック解除や完了したタスクの数の増加など、afterExecuteの終了後に問題が発生することがわかります。そのため、(processWorkerExitを呼び出すことにより)帯域幅を使用せずに(セマフォを解放することにより)タスクを入力できるようにしました。
ルイス・ギリェルメ

14

現在受け入れられている回答には、潜在的に重大な問題があります。これにより、ThreadPoolExecutor.executeの動作が変更され、 corePoolSize < maxPoolSize、ThreadPoolExecutorロジックがコアを超えてワーカーを追加することはありません。

ThreadPoolExecutor .execute(Runnableを):

    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);

具体的には、その最後の「else」ブロックはヒットしません。

より良い代替方法は、OPがすでに実行していることと同様のことを実行することです。RejectedExecutionHandlerを使用して同じputロジックを実行します。

public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    try {
        if (!executor.isShutdown()) {
            executor.getQueue().put(r);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RejectedExecutionException("Executor was interrupted while the task was waiting to put on work queue", e);
    }
}

コメント(この回答を参照)で指摘されているように、このアプローチでは注意すべき点がいくつかあります。

  1. 場合 corePoolSize==0、タスクが表示される前にプール内のすべてのスレッドが停止する可能性がある競合状態があります
  2. キュータスクをラップする実装(には適用されませんThreadPoolExecutor)を使用すると、ハンドラーも同じ方法でラップしない限り、問題が発生します。

これらの落とし穴を念頭に置いて、このソリューションは最も一般的なThreadPoolExecutorsで機能し、corePoolSize < maxPoolSize。の場合を適切に処理します。


反対票を投じた人に-洞察を提供できますか?この回答に間違った/誤解を招く/危険なものがありますか?私はあなたの懸念に対処する機会が欲しいです。
krease 2015年

2
私は反対票を投じませんでしたが、それは非常に悪い考えのようです
vanOekel 2015年

@ vanOekel-リンクに感謝します-その答えは、このアプローチを使用する場合に知っておくべきいくつかの有効なケースを提起しますが、IMOはそれを「非常に悪い考え」にしません-それでも現在受け入れられている答えに存在する問題を解決します。私はそれらの警告で私の答えを更新しました。
リース2015年

コアプールサイズが0で、タスクがエグゼキュータに送信された場合、タスクを処理するためにキューがいっぱいになると、エグゼキュータはスレッドの作成を開始します。では、なぜデッドロックが発生しやすいのでしょうか。あなたのポイントを取得できませんでした。詳しく教えていただけますか?
Farhan Shirgill Ansari 2016

@ ShirgillFarhanAnsari-前のコメントで提起されたケースです。キューに直接追加しても、スレッドの作成やワーカーの開始がトリガーされないために発生する可能性があります。これは、コアプールサイズをゼロ以外にすることで軽減できるエッジケース/競合状態です
Krease 2016年

4

これは古い質問ですが、新しいタスクの作成が非常に高速であり、既存のタスクが十分に速く完了しなかったためにOutOfMemoryErrorが多すぎると発生するという同様の問題がありました。

私の場合Callablesは送信され、結果が必要なので、によってFutures返されたすべてを保存する必要がありexecutor.submit()ます。私の解決策を置くことだったFuturesBlockingQueue最大サイズ。そのキューがいっぱいになると、いくつかが完了する(要素がキューから削除される)まで、タスクは生成されません。擬似コードの場合:

final ExecutorService executor = Executors.newFixedThreadPool(numWorkerThreads);
final LinkedBlockingQueue<Future> futures = new LinkedBlockingQueue<>(maxQueueSize);
try {   
    Thread taskGenerator = new Thread() {
        @Override
        public void run() {
            while (reader.hasNext) {
                Callable task = generateTask(reader.next());
                Future future = executor.submit(task);
                try {
                    // if queue is full blocks until a task
                    // is completed and hence no future tasks are submitted.
                    futures.put(compoundFuture);
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();         
                }
            }
        executor.shutdown();
        }
    }
    taskGenerator.start();

    // read from queue as long as task are being generated
    // or while Queue has elements in it
    while (taskGenerator.isAlive()
                    || !futures.isEmpty()) {
        Future compoundFuture = futures.take();
        // do something
    }
} catch (InterruptedException ex) {
    Thread.currentThread().interrupt();     
} catch (ExecutionException ex) {
    throw new MyException(ex);
} finally {
    executor.shutdownNow();
}

2

私も同様の問題を抱えていて、次のbeforeExecute/afterExecuteフックを使用して実装しましたThreadPoolExecutor

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Blocks current task execution if there is not enough resources for it.
 * Maximum task count usage controlled by maxTaskCount property.
 */
public class BlockingThreadPoolExecutor extends ThreadPoolExecutor {

    private final ReentrantLock taskLock = new ReentrantLock();
    private final Condition unpaused = taskLock.newCondition();
    private final int maxTaskCount;

    private volatile int currentTaskCount;

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

    /**
     * Executes task if there is enough system resources for it. Otherwise
     * waits.
     */
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        taskLock.lock();
        try {
            // Spin while we will not have enough capacity for this job
            while (maxTaskCount < currentTaskCount) {
                try {
                    unpaused.await();
                } catch (InterruptedException e) {
                    t.interrupt();
                }
            }
            currentTaskCount++;
        } finally {
            taskLock.unlock();
        }
    }

    /**
     * Signalling that one more task is welcome
     */
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        taskLock.lock();
        try {
            currentTaskCount--;
            unpaused.signalAll();
        } finally {
            taskLock.unlock();
        }
    }
}

これはあなたにとって十分なはずです。ところで、元の実装はタスクサイズに基づいていました。これは、1つのタスクが別のタスクよりも100倍大きくなる可能性があり、2つの大きなタスクを送信するとボックスが無駄になるためですが、1つの大きなタスクとたくさんの小さなタスクを実行しても問題ありませんでした。I / Oを多用するタスクが、このクラスを使用できるサイズとほぼ同じである場合は、お知らせください。サイズベースの実装を投稿します。

ThreadPoolExecutorPSjavadocを確認する必要があります。Doug Leaによる、簡単にカスタマイズする方法についてのすばらしいユーザーガイドです。


1
スレッドがbeforeExecute()でロックを保持し、それmaxTaskCount < currentTaskCountを確認してunpaused条件付きで待機を開始するとどうなるのでしょうか。同時に、別のスレッドがafterExecute()のロックを取得して、タスクの完了を通知しようとします。デッドロックではないでしょうか?
Tahir Akhtar 2010

1
また、このソリューションでは、キューがいっぱいになったときにタスクを送信するスレッドがブロックされないことにも気づきました。だからRejectedExecutionExceptionまだ可能です。
Tahir Akhtar 2010

1
ReentrantLock / Conditionクラスのセマンティックは、synchronized&wait / notifyが提供するものと似ています。条件待機メソッドが呼び出されると、ロックが解除されるため、デッドロックは発生しません。
Petro Semeniuk 2010

そうです、このExecutorServiceは、呼び出し元のスレッドをブロックせずに、送信時にタスクをブロックします。ジョブは送信されたばかりで、十分なシステムリソースがある場合は、非同期で処理されます。
Petro Semeniuk 2010

2

デコレータパターンに従い、セマフォを使用して実行されるタスクの数を制御するソリューションを実装しました。あなたはそれをどのようにでも使うことができますExecutor

  • 進行中のタスクの最大数を指定します
  • タスク実行許可を待機する最大タイムアウトを指定します(タイムアウトが経過し、許可が取得されない場合、aRejectedExecutionExceptionがスローされます)
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;

import javax.annotation.Nonnull;

public class BlockingOnFullQueueExecutorDecorator implements Executor {

    private static final class PermitReleasingDecorator implements Runnable {

        @Nonnull
        private final Runnable delegate;

        @Nonnull
        private final Semaphore semaphore;

        private PermitReleasingDecorator(@Nonnull final Runnable task, @Nonnull final Semaphore semaphoreToRelease) {
            this.delegate = task;
            this.semaphore = semaphoreToRelease;
        }

        @Override
        public void run() {
            try {
                this.delegate.run();
            }
            finally {
                // however execution goes, release permit for next task
                this.semaphore.release();
            }
        }

        @Override
        public final String toString() {
            return String.format("%s[delegate='%s']", getClass().getSimpleName(), this.delegate);
        }
    }

    @Nonnull
    private final Semaphore taskLimit;

    @Nonnull
    private final Duration timeout;

    @Nonnull
    private final Executor delegate;

    public BlockingOnFullQueueExecutorDecorator(@Nonnull final Executor executor, final int maximumTaskNumber, @Nonnull final Duration maximumTimeout) {
        this.delegate = Objects.requireNonNull(executor, "'executor' must not be null");
        if (maximumTaskNumber < 1) {
            throw new IllegalArgumentException(String.format("At least one task must be permitted, not '%d'", maximumTaskNumber));
        }
        this.timeout = Objects.requireNonNull(maximumTimeout, "'maximumTimeout' must not be null");
        if (this.timeout.isNegative()) {
            throw new IllegalArgumentException("'maximumTimeout' must not be negative");
        }
        this.taskLimit = new Semaphore(maximumTaskNumber);
    }

    @Override
    public final void execute(final Runnable command) {
        Objects.requireNonNull(command, "'command' must not be null");
        try {
            // attempt to acquire permit for task execution
            if (!this.taskLimit.tryAcquire(this.timeout.toMillis(), MILLISECONDS)) {
                throw new RejectedExecutionException(String.format("Executor '%s' busy", this.delegate));
            }
        }
        catch (final InterruptedException e) {
            // restore interrupt status
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }

        this.delegate.execute(new PermitReleasingDecorator(command, this.taskLimit));
    }

    @Override
    public final String toString() {
        return String.format("%s[availablePermits='%s',timeout='%s',delegate='%s']", getClass().getSimpleName(), this.taskLimit.availablePermits(),
                this.timeout, this.delegate);
    }
}

1

ArrayBlockingQueueaaの代わりに使うのと同じくらい簡単だと思いますLinkedBlockingQueue

私を無視してください...それは完全に間違っています。ThreadPoolExecutor電話しQueue#offerないputあなたが必要とする効果をもたらすであろうあり。

代わりにThreadPoolExecutorexecute(Runnable)その呼び出しの実装を拡張して提供することができますputofferます。

それは私が恐れている完全に満足のいく答えのようには思えません。

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