web-dev-qa-db-ja.com

時間がかかるデフォルトのForkJoinPoolエグゼキュータ

私は、リストソースから生成されたストリームの非同期実行のためにCompletableFutureを使用しています。

そのため、オーバーロードされたメソッド、つまりCompletableFutureの「supplyAsync」をテストしています。このメソッドでは、1つのメソッドが単一のサプライヤーパラメーターのみを受け取り、もう1つのメソッドがサプライヤーパラメーターとエグゼキューターパラメーターを受け取ります。両方のドキュメントは次のとおりです。

1

supplyAsync(サプライヤーサプライヤー)

ForkJoinPool.commonPool()で実行されているタスクによって、指定されたサプライヤを呼び出して取得した値で非同期に完了する新しいCompletableFutureを返します。

2番目

supplyAsync(サプライヤーサプライヤー、エグゼキューターエグゼキューター)

指定されたサプライヤを呼び出して取得した値を使用して、指定されたエグゼキュータで実行されているタスクによって非同期的に完了する新しいCompletableFutureを返します。

そして、これが私のテストクラスです:

public class TestCompleteableAndParallelStream {

    public static void main(String[] args) {
        List<MyTask> tasks = IntStream.range(0, 10)
                .mapToObj(i -> new MyTask(1))
                .collect(Collectors.toList());

        useCompletableFuture(tasks);

        useCompletableFutureWithExecutor(tasks);

    }

    public static void useCompletableFutureWithExecutor(List<MyTask> tasks) {
          long start = System.nanoTime();
          ExecutorService executor = Executors.newFixedThreadPool(Math.min(tasks.size(), 10));
          List<CompletableFuture<Integer>> futures =
              tasks.stream()
                   .map(t -> CompletableFuture.supplyAsync(() -> t.calculate(), executor))
                   .collect(Collectors.toList());

          List<Integer> result =
              futures.stream()
                     .map(CompletableFuture::join)
                     .collect(Collectors.toList());
          long duration = (System.nanoTime() - start) / 1_000_000;
          System.out.printf("Processed %d tasks in %d millis\n", tasks.size(), duration);
          System.out.println(result);
          executor.shutdown();
        }

    public static void useCompletableFuture(List<MyTask> tasks) {
          long start = System.nanoTime();
          List<CompletableFuture<Integer>> futures =
              tasks.stream()
                   .map(t -> CompletableFuture.supplyAsync(() -> t.calculate()))
                   .collect(Collectors.toList());

          List<Integer> result =
              futures.stream()
                     .map(CompletableFuture::join)
                     .collect(Collectors.toList());
          long duration = (System.nanoTime() - start) / 1_000_000;
          System.out.printf("Processed %d tasks in %d millis\n", tasks.size(), duration);
          System.out.println(result);
        }



}


class MyTask {
      private final int duration;
      public MyTask(int duration) {
        this.duration = duration;
      }
      public int calculate() {
        System.out.println(Thread.currentThread().getName());
        try {
          Thread.sleep(duration * 1000);
        } catch (final InterruptedException e) {
          throw new RuntimeException(e);
        }
        return duration;
      }
    }

「useCompletableFuture」メソッドの完了には約4秒かかりますが、「useCompletableFutureWithExecutor」メソッドの完了には1秒しかかかりません。

私の質問はありません、オーバーヘッドを実行できるForkJoinPool.commonPool()はどのような異なる処理をしますか?その中で、ForkJoinPoolよりもカスタムエグゼキュータプールを常に好むべきではありませんか?

7
KayV

ForkJoinPool.commonPool()サイズを確認してください。デフォルトでは、次のサイズのプールが作成されます

_Runtime.getRuntime().availableProcessors() - 1
_

私はあなたの例を私のInteli7-4800MQ(4コア+ 4仮想コア)で実行し、私の場合の共通プールのサイズは_7_なので、計算全体に約2000ミリ秒かかりました。

_ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-6
ForkJoinPool.commonPool-worker-5
ForkJoinPool.commonPool-worker-3
ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-1
Processed 10 tasks in 2005 millis
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
_

2番目のケースでは

_Executors.newFixedThreadPool(Math.min(tasks.size(), 10));
_

したがって、プールには計算を実行する準備ができている10個のスレッドがあるため、すべてのタスクは約1000ミリ秒で実行されます。

_pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5
pool-1-thread-6
pool-1-thread-7
pool-1-thread-8
pool-1-thread-9
pool-1-thread-10
Processed 10 tasks in 1002 millis
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
_

ForkJoinPoolExecutorServiceの違い

Eugene は、彼のコメントの中でもう1つ重要なことについても言及しています。 ForkJoinPoolは、作業を盗むアプローチを使用します。

ForkJoinPool は、主にワークスティーリングを採用しているという点で、他の種類のExecutorServiceとは異なります。プール内のすべてのスレッドは、プールに送信されたタスクを見つけて実行しようとします。および/または他のアクティブなタスクによって作成されます(最終的に、存在しない場合は作業の待機をブロックします)。これにより、ほとんどのタスクが他のサブタスクを生成する場合(ほとんどのForkJoinTasksと同様)、および多くの小さなタスクが外部クライアントからプールに送信される場合に、効率的な処理が可能になります。特にコンストラクターでasyncModeをtrueに設定する場合、ForkJoinPoolsは、決して結合されないイベントスタイルのタスクでの使用にも適している場合があります。

一方、 .newFixedThreadPool() で作成されたExecutorServiceは、分割統治法を使用します。

プールサイズを決定する方法は?

最適なスレッドプールサイズについて質問がありました。そこで役立つ情報が見つかるかもしれません。

スレッドプールの理想的なサイズの設定

また、このスレッドは調査するのに適した場所です。

Java 8並列ストリーム のカスタムスレッドプール

5
Szymon Stepniak

インターネットでソリューションをさらに確認したところ、次のプロパティを使用してForkJoinPoolが使用するデフォルトのプールサイズを変更できることがわかりました。

-Djava.util.concurrent.ForkJoinPool.common.parallelism=16

したがって、このプロパティは、ForkJoinPoolをより効率的な方法でより並列処理して利用できるようにするのにさらに役立ちます。

3
KayV