web-dev-qa-db-ja.com

ThreadPoolExecutorを取得して、キューに入れる前にスレッドを最大に増やす方法は?

多くの人が使用するThreadPoolExecutorスレッドプールをサポートするExecutorServiceのデフォルトの動作にしばらくイライラしています。 Javadocsから引用するには:

CorePoolSizeを超えて実行されているmaximumPoolSizeスレッドよりも少ない場合、新しいスレッドが作成されますキューがいっぱいの場合のみ

つまり、次のコードでスレッドプールを定義すると、LinkedBlockingQueueが無制限であるため、2番目のスレッドが never 開始されます。

ExecutorService threadPool =
   new ThreadPoolExecutor(1 /*core*/, 50 /*max*/, 60 /*timeout*/,
      TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(/* unlimited queue */));

bounded キューがあり、キューが full である場合のみ、コア番号を超えるスレッドが開始されます。多数のジュニアJavaマルチスレッドプログラマはThreadPoolExecutorのこの動作を認識していないと思われます。

今、私はこれが最適でない特定のユースケースを持っています。独自のTPEクラスを作成せずに、それを回避する方法を探しています。

私の要件は、おそらく信頼性の低いサードパーティにコールバックするWebサービスです。

  • Webリクエストと同期してコールバックを行いたくないので、スレッドプールを使用します。
  • 通常、1分間に2、3個取得するため、ほとんどが休止状態の多数のスレッドでnewFixedThreadPool(...)を使用したくありません。
  • 頻繁にこのトラフィックのバーストを取得し、スレッドの数を最大値(たとえば50)にスケールアップしたいと思います。
  • best を実行してすべてのコールバックを試行する必要があるので、50を超える追加コールバックをキューに入れたいと思います。 newCachedThreadPool()

ThreadPoolExecutorでこの制限を回避するにはどうすればよいでしょうか。キューをバインドし、] /より多くのスレッドを開始する必要があります。 キューイングタスクの前にさらにスレッドを開始するにはどうすればよいですか?

編集:

@Flavioは、ThreadPoolExecutor.allowCoreThreadTimeOut(true)を使用してコアスレッドのタイムアウトと終了を行うことについて良い点を示しています。私はそれを考えましたが、それでもコアスレッド機能が必要でした。可能であれば、プール内のスレッドの数がコアサイズを下回らないようにしました。

87
Gray

スレッドを開始する前にキューを制限していっぱいにする必要があるThreadPoolExecutorでこの制限を回避するにはどうすればよいですか。

ThreadPoolExecutorを使用して、この制限に対するややエレガントな(おそらく少しハッキングな)ソリューションをようやく見つけたと思います。 LinkedBlockingQueueを拡張して、いくつかのタスクが既にキューに入れられているときにqueue.offer(...)に対してfalseを返すようにします。現在のスレッドがキューに入れられたタスクに追いついていない場合、TPEは追加のスレッドを追加します。プールがすでに最大スレッドにある場合、RejectedExecutionHandlerが呼び出されます。その後、put(...)をキューに入れるハンドラーです。

offer(...)falseを返すことができ、put()がブロックしないキューを書くことは確かに奇妙です。それがハックの部分です。しかし、これはTPEのキューの使用ではうまく機能するので、これを行っても問題はありません。

コードは次のとおりです。

_// extend LinkedBlockingQueue to force offer() to return false conditionally
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    private static final long serialVersionUID = -6903933921423432194L;
    @Override
    public boolean offer(Runnable e) {
        /*
         * Offer it to the queue if there is 0 items already queued, else
         * return false so the TPE will add another thread. If we return false
         * and max threads have been reached then the RejectedExecutionHandler
         * will be called which will do the put into the queue.
         */
        if (size() == 0) {
            return super.offer(e);
        } else {
            return false;
        }
    }
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1 /*core*/, 50 /*max*/,
        60 /*secs*/, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            /*
             * This does the actual put into the queue. Once the max threads
             * have been reached, the tasks will then queue up.
             */
            executor.getQueue().put(r);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
        }
    }
});
_

このメカニズムを使用すると、タスクをキューに送信すると、ThreadPoolExecutorは次のことを行います。

  1. スレッド数を最初にコアサイズに拡大します(ここでは1)。
  2. キューに提供します。キューが空の場合、既存のスレッドによって処理されるようにキューに入れられます。
  3. キューに既に1つ以上の要素がある場合、offer(...)はfalseを返します。
  4. Falseが返された場合、最大数(ここでは50)に達するまでプール内のスレッドの数を増やします。
  5. 最大の場合、RejectedExecutionHandlerを呼び出します
  6. 次に、RejectedExecutionHandlerはタスクをキューに入れ、最初に利用可能なスレッドによってFIFOの順序で処理されるようにします。

上記の例のコードでは、キューは無制限ですが、制限付きキューとして定義することもできます。たとえば、LinkedBlockingQueueに1000の容量を追加すると、次のようになります。

  1. スレッドを最大まで拡大する
  2. 次に、1000のタスクでいっぱいになるまでキューに入れます
  3. 次に、キューが使用できるスペースができるまで呼び出し元をブロックします。

さらに、RejectedExecutionHandleroffer(...)を本当に使用する必要がある場合は、_Long.MAX_VALUE_をタイムアウトとして代わりにoffer(E, long, TimeUnit)メソッドを使用できます。

編集:

@Ralfのフィードバックごとにoffer(...)メソッドのオーバーライドを調整しました。これは、負荷に追いついていない場合にのみ、プール内のスレッド数をスケールアップします。

編集:

この答えのもう1つの微調整は、アイドルスレッドがあるかどうかを実際にTPEに尋ね、存在する場合にのみアイテムをキューに入れることです。このための真のクラスを作成し、ourQueue.setThreadPoolExecutor(tpe);メソッドを追加する必要があります。

offer(...)メソッドは次のようになります。

  1. tpe.getPoolSize() == tpe.getMaximumPoolSize()super.offer(...)を呼び出すだけかどうかを確認してください。
  2. それ以外の場合、tpe.getPoolSize() > tpe.getActiveCount()は、アイドルスレッドがあるように見えるため、super.offer(...)を呼び出します。
  3. それ以外の場合は、falseを返して別のスレッドをフォークします。

たぶんこれ:

_int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
    return super.offer(e);
} else {
    return false;
}
_

TPEのgetメソッドは、volatileフィールドにアクセスするか(getActiveCount()の場合)TPEをロックしてスレッドリストを調べるため、高価です。また、ここには競合状態があり、タスクが不適切にキューに入れられたり、アイドルスレッドがあったときに別のスレッドが分岐したりする可能性があります。

42
Gray

この質問には他にも2つの答えがありますが、これが一番いいと思います。

現在受け入れられている答え のテクニックに基づいています。つまり:

  1. キューのoffer()メソッドをオーバーライドして、(時々)falseを返します。
  2. ThreadPoolExecutorが新しいスレッドを生成するか、タスクを拒否します。
  3. RejectedExecutionHandler実際にに設定し、拒否時にタスクをキューに入れます。

問題は、offer()がfalseを返す必要がある場合です。現在受け入れられている答えは、キューにタスクがいくつかある場合にfalseを返しますが、コメントで指摘したように、これは望ましくない結果を引き起こします。あるいは、常にfalseを返す場合、キューで待機しているスレッドがある場合でも、新しいスレッドを生成し続けます。

解決策は、Java 7 LinkedTransferQueue を使用し、offer() call tryTransfer()を使用することです。それ以外の場合、offer()はfalseを返し、ThreadPoolExecutorは新しいスレッドを生成します。

    BlockingQueue<Runnable> queue = new LinkedTransferQueue<Runnable>() {
        @Override
        public boolean offer(Runnable e) {
            return tryTransfer(e);
        }
    };
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue);
    threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    });
25

コアサイズと最大サイズを同じ値に設定し、allowCoreThreadTimeOut(true)を使用してコアスレッドをプールから削除できるようにします。

24
Flavio

注:現在、私は my other answer を好み、推奨しています。

これは私にもっと簡単に感じられるバージョンです:新しいタスクが実行されるたびにcorePoolSizeを(maximumPoolSizeの制限まで)増やし、その後、タスクが完了します。

別の言い方をすれば、実行中またはキューに入れられたタスクの数を追跡し、ユーザーが指定した「コアプールサイズ」とmaximumPoolSizeの間である限り、corePoolSizeがタスクの数と等しくなるようにします。

_public class GrowBeforeQueueThreadPoolExecutor extends ThreadPoolExecutor {
    private int userSpecifiedCorePoolSize;
    private int taskCount;

    public GrowBeforeQueueThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        userSpecifiedCorePoolSize = corePoolSize;
    }

    @Override
    public void execute(Runnable runnable) {
        synchronized (this) {
            taskCount++;
            setCorePoolSizeToTaskCountWithinBounds();
        }
        super.execute(runnable);
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable throwable) {
        super.afterExecute(runnable, throwable);
        synchronized (this) {
            taskCount--;
            setCorePoolSizeToTaskCountWithinBounds();
        }
    }

    private void setCorePoolSizeToTaskCountWithinBounds() {
        int threads = taskCount;
        if (threads < userSpecifiedCorePoolSize) threads = userSpecifiedCorePoolSize;
        if (threads > getMaximumPoolSize()) threads = getMaximumPoolSize();
        setCorePoolSize(threads);
    }
}
_

書かれているように、クラスは、構築後にユーザーが指定したcorePoolSizeまたはmaximumPoolSizeの変更をサポートせず、直接またはremove()またはpurge()を介した作業キューの操作をサポートしません。

追加のThreadPoolExecutorを取り、creationThresholdをオーバーライドするexecuteのサブクラスがあります。

public void execute(Runnable command) {
    super.execute(command);
    final int poolSize = getPoolSize();
    if (poolSize < getMaximumPoolSize()) {
        if (getQueue().size() > creationThreshold) {
            synchronized (this) {
                setCorePoolSize(poolSize + 1);
                setCorePoolSize(poolSize);
            }
        }
    }
}

多分それも助けになるかもしれませんが、もちろんあなたのものはもっと芸術的です...

6
Ralf H

推奨される回答は、JDKスレッドプールの問題のうち1つだけを解決します。

  1. JDKスレッドプールはキューイングに偏っています。したがって、新しいスレッドを作成する代わりに、タスクをキューに入れます。キューが制限に達した場合のみ、スレッドプールは新しいスレッドを生成します。

  2. 負荷が軽くなると、スレッドの廃止は行われません。たとえば、プールに到達するジョブのバーストが原因でプールが最大になり、その後に一度に最大2タスクの軽負荷が発生した場合、プールはすべてのスレッドを使用して軽負荷を処理し、スレッドのリタイアを防ぎます。 (必要なスレッドは2つだけです...)

上記の動作に不満があるため、先に進み、上記の欠陥を克服するためにプールを実装しました。

解決するには2)Lifoスケジューリングを使用すると、問題が解決します。このアイデアは、ACM applicative 2015カンファレンスでBen Maurerによって提示されました: Systems @ Facebook scale

そこで、新しい実装が生まれました。

LifoThreadPoolExecutorSQP

これまでのところ、この実装は [〜#〜] zel [〜#〜] の非同期実行パフォーマンスを改善します。

実装はスピン可能で、コンテキストスイッチのオーバーヘッドを削減し、特定のユースケースで優れたパフォーマンスを実現します。

それが役に立てば幸い...

PS:JDKフォーク結合プールはExecutorServiceを実装し、「通常の」スレッドプールとして機能します。実装はパフォーマンスが高く、LIFOスレッドスケジューリングを使用しますが、内部キューサイズ、リタイアタイムアウトを制御できません。 ..、そして最も重要なことには、タスクをキャンセルするときにタスクを中断できない

3
user2179737

注:現在、私は my other answer を好み、推奨しています。

キューを変更してfalseを返すという元のアイデアに従って、別の提案があります。この1つでは、すべてのタスクがキューに入ることができますが、タスクがexecute()の後にキューに入れられるたびに、キューが拒否するセンチネルno-opタスクが続き、新しいスレッドが生成され、実行されますno-opの直後にキューから何かが続きます。

ワーカースレッドはLinkedBlockingQueueを新しいタスクのためにポーリングしている可能性があるため、利用可能なスレッドがある場合でもタスクがキューに登録される可能性があります。使用可能なスレッドがある場合でも新しいスレッドを生成しないようにするには、キューで新しいタスクを待機しているスレッドの数を追跡し、キューに待機中のスレッドよりも多くのタスクがある場合にのみ新しいスレッドを生成する必要があります。

final Runnable SENTINEL_NO_OP = new Runnable() { public void run() { } };

final AtomicInteger waitingThreads = new AtomicInteger(0);

BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    @Override
    public boolean offer(Runnable e) {
        // offer returning false will cause the executor to spawn a new thread
        if (e == SENTINEL_NO_OP) return size() <= waitingThreads.get();
        else return super.offer(e);
    }

    @Override
    public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.poll(timeout, unit);
        } finally {
            waitingThreads.decrementAndGet();
        }
    }

    @Override
    public Runnable take() throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.take();
        } finally {
            waitingThreads.decrementAndGet();
        }
    }
};

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue) {
    @Override
    public void execute(Runnable command) {
        super.execute(command);
        if (getQueue().size() > waitingThreads.get()) super.execute(SENTINEL_NO_OP);
    }
};
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (r == SENTINEL_NO_OP) return;
        else throw new RejectedExecutionException();            
    }
});

私が考えることができる最良の解決策は、拡張することです。

ThreadPoolExecutorには、beforeExecuteおよびafterExecuteといういくつかのフックメソッドがあります。拡張機能では、タスクをフィードするために境界キューを使用し、オーバーフローを処理するために2番目の非境界キューを使用し続けることができます。誰かがsubmitを呼び出すと、制限されたキューにリクエストを配置することができます。例外が発生した場合は、タスクをオーバーフローキューに入れるだけです。次に、afterExecuteフックを使用して、タスクの終了後にオーバーフローキューに何かがあるかどうかを確認できます。このようにして、エグゼキュータはまずその制限されたキューの内容を処理し、時間が許せばこの無制限のキューから自動的にプルします。

あなたのソリューションよりも多くの作業のように見えますが、少なくともキューに予期しない動作を与えることは含まれていません。また、スローにかなり時間がかかる例外に依存するのではなく、キューとスレッドのステータスをチェックするより良い方法があると思います。

0
bstempi