web-dev-qa-db-ja.com

キューがいっぱいのときにThreadPoolExecutorブロック?

ThreadPoolExecutorを使用して多くのタスクを実行しようとしています。以下は架空の例です。

def workQueue = new ArrayBlockingQueue<Runnable>(3, false)
def threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.HOURS, workQueue)
for(int i = 0; i < 100000; i++)
    threadPoolExecutor.execute(runnable)

問題は、すぐにJava.util.concurrent.RejectedExecutionExceptionタスクの数が作業キューのサイズを超えているため。ただし、私が探している望ましい動作は、キューに空きができるまでメインスレッドをブロックすることです。これを達成する最良の方法は何ですか?

52
ghempton

非常に狭い状況では、必要なことを行うJava.util.concurrent.RejectedExecutionHandlerを実装できます。

RejectedExecutionHandler block = new RejectedExecutionHandler() {
  rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
     executor.getQueue().put( r );
  }
};

ThreadPoolExecutor pool = new ...
pool.setRejectedExecutionHandler(block);

今。これは非常に悪い考え以下の理由によります

  • キューに入れたものが見える前にプール内のすべてのスレッドが死んでしまう可能性があるため、デッドロックが発生しやすくなります。妥当なキープアライブ時間を設定して、これを軽減します。
  • タスクは、Executorが期待する方法でラップされません。多くのエグゼキューターの実装は、実行前に何らかの追跡オブジェクトでタスクをラップします。あなたのソースを見てください。
  • GetQueue()を介して追加することはAPIによって強く推奨されておらず、ある時点で禁止される場合があります。

ほぼ常に優れた戦略は、execute()を呼び出しているスレッドでタスクを実行することでアプリを調整するThreadPoolExecutor.CallerRunsPolicyをインストールすることです。

ただし、固有のリスクをすべて備えたブロッキング戦略が本当に必要な場合もあります。これらの条件下で言う

  • Execute()を呼び出すスレッドは1つだけです
  • キューの長さを非常に短くする必要がある(またはしたい)
  • 絶対にこの作業を実行するスレッドの数を制限する必要があり(通常は外部の理由により)、呼び出し側の実行戦略はそれを打ち破ります。
  • タスクのサイズは予測できないため、4つの短いタスクでプールが一時的にビジー状態になり、executeを呼び出している1つのスレッドが大きなものでスタックした場合、呼び出し側は飢sを引き起こす可能性があります。

だから、私が言うように。それはめったに必要ではなく、危険な場合がありますが、そこに行きます。

幸運を。

60
Darren Gilroy

semaphoreを使用して、スレッドがプールに入るのをブロックできます。

ExecutorService service = new ThreadPoolExecutor(
    3, 
    3, 
    1, 
    TimeUnit.HOURS, 
    new ArrayBlockingQueue<>(6, false)
);

Semaphore lock = new Semaphore(6); // equal to queue capacity

for (int i = 0; i < 100000; i++ ) {
    try {
        lock.acquire();
        service.submit(() -> {
            try {
              task.run();
            } finally {
              lock.release();
            }
        });
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

いくつかの落とし穴

  • このパターンは、固定スレッドプールでのみ使用してください。キューが頻繁にいっぱいになる可能性は低いため、新しいスレッドは作成されません。詳細については、Java ThreadPoolExecutorのドキュメントを参照してください。 https://docs.Oracle.com/javase/8/docs/api/Java/util/concurrent/ThreadPoolExecutor。 html これを回避する方法はありますが、この答えの範囲外です。
  • キューのサイズは、コアスレッドの数よりも大きくする必要があります。キューサイズを3にすると、最終的には次のようになります。

    • T0:3つのスレッドすべてが作業を行っており、キューは空であり、許可はありません。
    • T1:スレッド1が終了し、許可を解放します。
    • T2:スレッド1は、新しい作業のためにキューをポーリングし、何も検出せず、waits
    • T3:メインスレッドが作業をプールに送信し、スレッド1が作業を開始します。

    上記の例は、メインスレッドblockingスレッド1のスレッドに変換されます。小さな期間のように思えるかもしれませんが、現在は頻度を日と月で乗算します。突然の短い期間のすべてが、膨大な時間の浪費につながります。

5
devkaoru

する必要があるのは、ThreadPoolExecutorをExecutorにラップして、その中で同時に実行される操作の量を明示的に制限することです。

 private static class BlockingExecutor implements Executor {

    final Semaphore semaphore;
    final Executor delegate;

    private BlockingExecutor(final int concurrentTasksLimit, final Executor delegate) {
        semaphore = new Semaphore(concurrentTasksLimit);
        this.delegate = delegate;
    }

    @Override
    public void execute(final Runnable command) {
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
            return;
        }

        final Runnable wrapped = () -> {
            try {
                command.run();
            } finally {
                semaphore.release();
            }
        };

        delegate.execute(wrapped);

    }
}

ConcurrentTasksLimitをデリゲートエグゼキューターのthreadPoolSize + queueSizeに調整すると、問題をほぼ解決できます。

5

この場合のコードスニペットは次のとおりです。

public void executeBlocking( Runnable command ) {
    if ( threadPool == null ) {
        logger.error( "Thread pool '{}' not initialized.", threadPoolName );
        return;
    }
    ThreadPool threadPoolMonitor = this;
    boolean accepted = false;
    do {
        try {
            threadPool.execute( new Runnable() {
                @Override
                public void run() {
                    try {
                        command.run();
                    }
                    // to make sure that the monitor is freed on exit
                    finally {
                        // Notify all the threads waiting for the resource, if any.
                        synchronized ( threadPoolMonitor ) {
                            threadPoolMonitor.notifyAll();
                        }
                    }
                }
            } );
            accepted = true;
        }
        catch ( RejectedExecutionException e ) {
            // Thread pool is full
            try {
                // Block until one of the threads finishes its job and exits.
                synchronized ( threadPoolMonitor ) {
                    threadPoolMonitor.wait();
                }
            }
            catch ( InterruptedException ignored ) {
                // return immediately
                break;
            }
        }
    } while ( !accepted );
}

threadPoolは、既に初期化されているJava.util.concurrent.ExecutorServiceのローカルインスタンスです。

1
MoeHoss

これは私がやったことです:

int NUM_THREADS = 6;
Semaphore lock = new Semaphore(NUM_THREADS);
ExecutorService pool = Executors.newCachedThreadPool();

for (int i = 0; i < 100000; i++) {
    try {
        lock.acquire();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    pool.execute(() -> {
        try {
            // Task logic
        } finally {
            lock.release();
        }
    });
}
1
disrupt

カスタムRejectedExecutionHandlerを使用してこの問題を解決しました。これは、呼び出しスレッドをしばらくブロックするだけで、タスクを再度送信しようとします。

public class BlockWhenQueueFull implements RejectedExecutionHandler {

    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

        // The pool is full. Wait, then try again.
        try {
            long waitMs = 250;
            Thread.sleep(waitMs);
        } catch (InterruptedException interruptedException) {}

        executor.execute(r);
    }
}

このクラスは、スレッドプールエグゼキューターで、他のRejectedExecutionHandlerとして使用できます。この例では:

executorPool = new def threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.HOURS, workQueue, new BlockWhenQueueFull())

私が見る唯一の欠点は、呼び出しスレッドが厳密に必要な時間よりもわずかに長くロックされる可能性があることです(最大250ミリ秒)。多くの短時間実行タスクでは、おそらく待機時間を10ミリ秒程度に減らします。さらに、このエグゼキューターは事実上再帰的に呼び出されるため、スレッドが使用可能になるまでの非常に長い待機(時間)により、スタックオーバーフローが発生する可能性があります。

それにもかかわらず、私は個人的にこの方法が好きです。コンパクトで理解しやすく、うまく機能します。重要なものがありませんか?

0
TinkerTank