web-dev-qa-db-ja.com

Java 8パラレルストリームのカスタムスレッドプール

Java 8 パラレルストリーム のカスタムスレッドプールを指定することは可能ですか?私はそれをどこにも見つけることができません。

サーバーアプリケーションがあり、並列ストリームを使用したいとします。しかし、アプリケーションは大規模でマルチスレッド化されているので、区画化したいと思います。私はapplicationblockタスクの1つのモジュールで実行速度の遅いタスクが別のモジュールからしたくありません。

モジュールごとに異なるスレッドプールを使用できない場合は、現実世界のほとんどの状況で安全にパラレルストリームを使用できないことを意味します。

次の例を試してください。別々のスレッドで実行されるCPU集中型のタスクがいくつかあります。タスクは並列ストリームを活用します。最初のタスクは中断されるので、各ステップは1秒かかります(スレッドのスリープによってシミュレートされます)。問題は、他のスレッドが動かなくなり、壊れたタスクが終了するのを待つことです。これは人為的な例ですが、サーブレットアプリケーションと、実行時間の長いタスクを共有フォーク結合プールに送信する人を想像してみてください。

public class ParallelTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();

        es.execute(() -> runTask(1000)); //incorrect task
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));


        es.shutdown();
        es.awaitTermination(60, TimeUnit.SECONDS);
    }

    private static void runTask(int delay) {
        range(1, 1_000_000).parallel().filter(ParallelTest::isPrime).peek(i -> Utils.sleep(delay)).max()
                .ifPresent(max -> System.out.println(Thread.currentThread() + " " + max));
    }

    public static boolean isPrime(long n) {
        return n > 1 && rangeClosed(2, (long) sqrt(n)).noneMatch(divisor -> n % divisor == 0);
    }
}
338
Lukas

実際には、特定のfork-joinプールで並列操作を実行する方法があります。分岐結合プールでタスクとして実行すると、そこに留まり、一般的なものは使用されません。

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
forkJoinPool.submit(() ->
    //parallel task here, for example
    IntStream.range(1, 1_000_000).parallel().filter(PrimesPrint::isPrime).collect(toList())
).get();

このトリックは ForkJoinTask.fork に基づいています。

332
Lukas

Runtime.getRuntime().availableProcessors()が返すように、パラレルストリームはデフォルトのForkJoinPool.commonPool which デフォルトではプロセッサが1つ少ないので、スレッドが1つ少なくなります を使用します(つまり、パラレルストリームはメインスレッドも使用するのですべてのプロセッサを使用します)。

個別のプールまたはカスタムプールを必要とするアプリケーションの場合、ForkJoinPoolは特定のターゲット並列処理レベルで構築できます。デフォルトでは、利用可能なプロセッサの数と同じです。

これはまた、ネストされたパラレルストリームまたは複数のパラレルストリームが同時に開始された場合、それらはすべてshare同じプールです。長所:デフォルト(利用可能なプロセッサ数)を超えて使用することは決してありません。開始した各並列ストリームに割り当てられた「すべてのプロセッサ」を取得します(偶然に複数の場合)(これを回避するには ManagedBlocker を使用できます)。

並列ストリームの実行方法を変更するには、

  • パラレルストリームの実行を自分のForkJoinPoolに送信します。yourFJP.submit(() -> stream.parallel().forEach(soSomething)).get();または
  • システムプロパティを使用して共通プールのサイズを変更できます。ターゲットスレッドが20スレッドの場合はSystem.setProperty("Java.util.concurrent.ForkJoinPool.common.parallelism", "20")

8プロセッサを持っている私のマシン上の後者の例。次のプログラムを実行したとします。

long start = System.currentTimeMillis();
IntStream s = IntStream.range(0, 20);
//System.setProperty("Java.util.concurrent.ForkJoinPool.common.parallelism", "20");
s.parallel().forEach(i -> {
    try { Thread.sleep(100); } catch (Exception ignore) {}
    System.out.print((System.currentTimeMillis() - start) + " ");
});

出力は以下のとおりです。

215 216 216 216 216 216 216 216 315 316 316 316 316 316 316 316 415 416 416 416

つまり、パラレルストリームは一度に8つの項目を処理します。つまり、8つのスレッドを使用します。ただし、コメント行のコメントを解除すると、出力は次のようになります。

215 215 215 215 215 216 216 216 216 216 216 216 216 216 216 216 216 216 216 216 216

今回は、並列ストリームは20個のスレッドを使用し、ストリーム内の20個のエレメントすべてが同時に処理されました。

170
assylias

独自のforkJoinPool内で並列計算を起動するというトリックの代わりに、次のようにそのプールをCompletableFuture.supplyAsyncメソッドに渡すこともできます。

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
CompletableFuture<List<Integer>> primes = CompletableFuture.supplyAsync(() ->
    //parallel task here, for example
    range(1, 1_000_000).parallel().filter(PrimesPrint::isPrime).collect(toList()), 
    forkJoinPool
);
33
Mario Fusco

ForkJoinPoolを使用してパラレルストリームを送信しても、すべてのスレッドが確実に使用されるわけではありません。これ( HashSetからのパラレルストリームがパラレルで実行されない )とこれ( パラレルストリームがForkJoinPoolのすべてのスレッドを使用しないのはなぜですか? )を見ると、推論.

ショートバージョン:ForkJoinPool/submitがうまくいかない場合は、を使用してください。

System.setProperty("Java.util.concurrent.ForkJoinPool.common.parallelism", "10");
17
Tod Casasent

今まで、私はこの質問の答えに記述されている解決策を使いました。さて、私は Parallel Stream Support と呼ばれる小さなライブラリを思いつきました。

ForkJoinPool pool = new ForkJoinPool(NR_OF_THREADS);
ParallelIntStreamSupport.range(1, 1_000_000, pool)
    .filter(PrimesPrint::isPrime)
    .collect(toList())

しかし@PabloMatiasGomezがコメントで指摘しているように、共通プールのサイズに大きく依存する並列ストリームの分割メカニズムに関しては欠点があります。 を参照してください。HashSetからの並列ストリームは、並列では実行されません

仕事の種類ごとに別々のプールを用意するためだけにこのソリューションを使用していますが、使用しなくても共通プールのサイズを1に設定することはできません。

7
Stefan Ferstl

実際に使用されているスレッド数を測定するには、Thread.activeCount()をチェックします。

    Runnable r = () -> IntStream
            .range(-42, +42)
            .parallel()
            .map(i -> Thread.activeCount())
            .max()
            .ifPresent(System.out::println);

    ForkJoinPool.commonPool().submit(r).join();
    new ForkJoinPool(42).submit(r).join();

これは4コアCPUで次のような出力を生成することができます。

5 // common pool
23 // custom pool

.parallel()がなければ、

3 // common pool
4 // custom pool
7
charlie

注: カスタムスレッドプールが予想されるスレッド数を確実に使用するようにJDK 10で実装された修正があるようです。

カスタムForkJoinPool内での並列ストリーム実行は、並列処理に従う必要があります https://bugs.openjdk.Java.net/browse/JDK-8190974

4
Scott Langley

次のプロパティを使用してデフォルトの並列処理を変更できます。

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

これは、より多くの並列処理を使用するように設定できます。

4
KayV

AbacusUtil にアクセスしてください。スレッド番号はパラレルストリームに指定できます。これがサンプルコードです。

LongStream.range(4, 1_000_000).parallel(threadNum)...

開示:私はAbacusUtilの開発者です。

1
user_3380739

実装のハックに頼らないのであれば、mapcollectのセマンティクスを組み合わせたカスタムコレクターを実装することで同じことを実現する方法が常にあります。ForkJoinPoolに限定されることはありません。

list.stream()
  .collect(parallelToList(i -> fetchFromDb(i), executor))
  .join()

幸いなことに、それはすでにここで行われていてMaven Centralで利用可能です: http://github.com/pivovarit/parallel-collectors

免責事項:私はそれを書いて責任を負います。

0

cyclops-react でサードパーティのライブラリを使用しても構わない場合は、同じパイプライン内でシーケンシャルストリームとパラレルストリームを混在させてカスタムForkJoinPoolsを提供できます。例えば

 ReactiveSeq.range(1, 1_000_000)
            .foldParallel(new ForkJoinPool(10),
                          s->s.filter(i->true)
                              .peek(i->System.out.println("Thread " + Thread.currentThread().getId()))
                              .max(Comparator.naturalOrder()));

あるいは連続したストリーム内で処理を続けたい場合

 ReactiveSeq.range(1, 1_000_000)
            .parallel(new ForkJoinPool(10),
                      s->s.filter(i->true)
                          .peek(i->System.out.println("Thread " + Thread.currentThread().getId())))
            .map(this::processSequentially)
            .forEach(System.out::println);

[私はcyclops-reactの主導的開発者です]

0
John McClean

あなたがカスタムThreadPoolを必要としないがあなたがむしろ同時タスクの数を制限したいならば、あなたは使うことができます:

List<Path> paths = List.of("/path/file1.csv", "/path/file2.csv", "/path/file3.csv").stream().map(e -> Paths.get(e)).collect(toList());
List<List<Path>> partitions = Lists.partition(paths, 4); // Guava method

partitions.forEach(group -> group.parallelStream().forEach(csvFilePath -> {
       // do your processing   
}));

(これを尋ねる重複した質問はロックされています。

0
Martin Vseticka

プールサイズを調整するために、 custom ForkJoinPoolを次のように試してみました。

private static Set<String> ThreadNameSet = new HashSet<>();
private static Callable<Long> getSum() {
    List<Long> aList = LongStream.rangeClosed(0, 10_000_000).boxed().collect(Collectors.toList());
    return () -> aList.parallelStream()
            .peek((i) -> {
                String threadName = Thread.currentThread().getName();
                ThreadNameSet.add(threadName);
            })
            .reduce(0L, Long::sum);
}

private static void testForkJoinPool() {
    final int parallelism = 10;

    ForkJoinPool forkJoinPool = null;
    Long result = 0L;
    try {
        forkJoinPool = new ForkJoinPool(parallelism);
        result = forkJoinPool.submit(getSum()).get(); //this makes it an overall blocking call

    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    } finally {
        if (forkJoinPool != null) {
            forkJoinPool.shutdown(); //always remember to shutdown the pool
        }
    }
    out.println(result);
    out.println(ThreadNameSet);
}

これは、プールがデフォルトの 4 よりも多くのスレッドを使用しているという出力です。

50000005000000
[ForkJoinPool-1-worker-8, ForkJoinPool-1-worker-9, ForkJoinPool-1-worker-6, ForkJoinPool-1-worker-11, ForkJoinPool-1-worker-10, ForkJoinPool-1-worker-1, ForkJoinPool-1-worker-15, ForkJoinPool-1-worker-13, ForkJoinPool-1-worker-4, ForkJoinPool-1-worker-2]

しかし、実際には、次のようにThreadPoolExecutorを使って同じ結果を得ようとすると、 weirdo があります。

BlockingDeque blockingDeque = new LinkedBlockingDeque(1000);
ThreadPoolExecutor fixedSizePool = new ThreadPoolExecutor(10, 20, 60, TimeUnit.SECONDS, blockingDeque, new MyThreadFactory("my-thread"));

しかし私は失敗しました。

それは新しいスレッドで parallelStream を開始するだけで、それ以外はすべて同じです。 again parallelStreamがその開始に ForkJoinPool を使用することを証明します。子スレッド.

0
Hearen