web-dev-qa-db-fra.com

Pool de threads personnalisé dans un flux parallèle Java 8

Est-il possible de spécifier un pool de threads personnalisé pour Java 8 flux parallèle ? Je ne peux pas le trouver nulle part. 

Imaginez que j'ai une application serveur et que je souhaite utiliser des flux parallèles. Mais l’application est volumineuse et multi-thread alors je veux la compartimenter. Je ne veux pas d'une tâche lente dans un module des tâches d'applicationblock d'un autre module.

Si je ne peux pas utiliser différents pools de threads pour différents modules, cela signifie que je ne peux pas utiliser en toute sécurité des flux parallèles dans la plupart des situations du monde réel.

Essayez l'exemple suivant. Certaines tâches gourmandes en ressources processeur sont exécutées dans des threads distincts . Les tâches exploitent des flux parallèles. La première tâche est interrompue, chaque étape dure donc 1 seconde (simulée par la veille du thread). Le problème est que d'autres threads restent bloqués et attendent que la tâche interrompue se termine. Ceci est un exemple artificiel, mais imaginons une application de servlet et une personne soumettant une tâche longue en cours d'exécution au pool de jointure de partage partagé. 

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);
    }
}
326
Lukas

Il existe en réalité une astuce: exécuter une opération parallèle dans un pool spécifique. Si vous l'exécutez en tant que tâche dans un pool fork-join, elle y reste et n'utilise pas le pool commun. 

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();

L'astuce est basée sur ForkJoinTask.fork qui spécifie: "Permet à cette tâche d'exécuter cette tâche de manière asynchrone dans le pool dans lequel la tâche actuelle est en cours d'exécution, ou à l'aide de ForkJoinPool.commonPool () sinon deForkJoinPool ()"

323
Lukas

Les flux parallèles utilisent le ForkJoinPool.commonPool par défaut qui par défaut a un nombre de threads de moins que vous avez de processeurs , tel que retourné par Runtime.getRuntime().availableProcessors() (Cela signifie que les flux parallèles utilisent tous vos processeurs car ils utilisent également le thread principal):

Pour les applications nécessitant des pools distincts ou personnalisés, un ForkJoinPool peut être construit avec un niveau de parallélisme cible donné; par défaut, égal au nombre de processeurs disponibles.

Cela signifie également que si vous avez des flux parallèles imbriqués ou si plusieurs flux parallèles ont démarré simultanément, ils partageront tous le même pool share. Avantage: vous n'utiliserez jamais plus que la valeur par défaut (nombre de processeurs disponibles). Inconvénient: vous ne pouvez pas obtenir "tous les processeurs" attribués à chaque flux parallèle que vous lancez (si vous en avez plusieurs). (Apparemment, vous pouvez utiliser un ManagedBlocker pour le contourner.)

Pour changer la façon dont les flux parallèles sont exécutés, vous pouvez soit:

  • soumettez l'exécution du flux parallèle à votre propre ForkJoinPool: yourFJP.submit(() -> stream.parallel().forEach(soSomething)).get(); ou
  • vous pouvez modifier la taille du pool commun à l'aide des propriétés système: System.setProperty("Java.util.concurrent.ForkJoinPool.common.parallelism", "20") pour un parallélisme cible de 20 unités d'exécution.

Exemple de ce dernier sur ma machine qui a 8 processeurs. Si je lance le programme suivant:

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) + " ");
});

La sortie est:

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

Vous pouvez donc voir que le flux parallèle traite 8 éléments à la fois, c’est-à-dire qu’il utilise 8 threads. Cependant, si je supprime la mise en commentaire de la ligne commentée, le résultat est le suivant:

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

Cette fois, le flux parallèle a utilisé 20 threads et tous les 20 éléments du flux ont été traités simultanément.

166
assylias

Sinon, vous pouvez également transmettre ce pool à la méthode CompletableFuture.supplyAsync, comme dans:

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
);
32
Mario Fusco

L'utilisation d'un ForkJoinPool et la soumission d'un flux parallèle n'utilisent pas de manière fiable tous les threads. Si vous regardez ceci ( Le flux parallèle d'un HashSet ne fonctionne pas en parallèle ) et ceci ( Pourquoi le flux parallèle n'utilise-t-il pas tous les threads de ForkJoinPool? ), Vous verrez raisonnement.

Version courte: si ForkJoinPool/submit ne fonctionne pas pour vous, utilisez 

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

Jusqu'à présent, j'ai utilisé les solutions décrites dans les réponses à cette question. Maintenant, je suis venu avec une petite bibliothèque appelée Parallel Stream Support pour cela:

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

Mais comme l'a souligné @PabloMatiasGomez dans les commentaires, le mécanisme de division des flux parallèles présente des inconvénients, qui dépendent fortement de la taille du pool commun. Voir Le flux parallèle d'un HashSet ne s'exécute pas en parallèle .

J'utilise cette solution uniquement pour avoir des pools distincts pour différents types de travail, mais je ne peux pas définir la taille du pool commun sur 1, même si je ne l'utilise pas.

7
Stefan Ferstl

Pour mesurer le nombre réel de threads utilisés, vous pouvez vérifier 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();

Cela peut produire sur un processeur à 4 cœurs une sortie du type:

5 // common pool
23 // custom pool

Sans .parallel() cela donne:

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

Remarque: Un correctif implémenté dans JDK 10 garantit que le pool de threads personnalisé utilise le nombre de threads prévu.

L'exécution de flux parallèles dans un ForkJoinPool personnalisé doit obéir au parallélisme https://bugs.openjdk.Java.net/browse/JDK-8190974

3
Scott Langley

Allez chercher AbacusUtil . Le nombre de threads peut être spécifié pour le flux parallèle. Voici l exemple de code:

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

Divulgation: Je suis le développeur de AbacusUtil.

1
user_3380739

Si vous ne voulez pas vous fier aux piratages d'implémentation, il existe toujours un moyen d'obtenir les mêmes résultats en implémentant des collecteurs personnalisés qui associent la sémantique map et collect ... et vous ne serez pas limité à ForkJoinPool:

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

Heureusement, c'est déjà fait ici et disponible sur Maven Central: http://github.com/pivovarit/parallel-collectors

Disclaimer: Je l'ai écrit et en assume la responsabilité.

0
Grzegorz Piwowarek

Nous pouvons changer le parallélisme par défaut en utilisant la propriété suivante:

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

qui peut mettre en place pour utiliser plus de parallélisme.

0
KayV

Si cela ne vous dérange pas d'utiliser une bibliothèque tierce, avec cyclops-react , vous pouvez mélanger des flux séquentiels et parallèles dans le même pipeline et fournir des ForkJoinPools personnalisés. Par exemple

 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()));

Ou si nous souhaitions continuer le traitement dans un flux séquentiel

 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);

[Divulgation Je suis le développeur principal de cyclops-react]

0
John McClean

Si vous n'avez pas besoin d'un ThreadPool personnalisé mais souhaitez plutôt limiter le nombre de tâches simultanées, vous pouvez utiliser:

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   
}));

(Une copie de la question demandant ceci est verrouillée, alors veuillez me laisser ici)

0
Martin Vseticka

J'ai essayé le custom ForkJoinPool comme suit pour ajuster la taille de la piscine:

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);
}

Voici la sortie indiquant que le pool utilise plus de threads que la valeur par défaut 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]

Mais en réalité, il existe un weirdo , lorsque j’ai essayé d’obtenir le même résultat en utilisant ThreadPoolExecutor comme suit:

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

mais j'ai échoué. 

Cela démarrera seulement le parallelStream dans un nouveau fil, puis tout le reste sera identique, ce qui prouvera que encore prouve que la parallelStream utilisera le ForkJoinPool pour démarrer ses fils. 

0
Hearen