Conjunto de encadeamentos customizados no fluxo paralelo do Java 8


398

É possível especificar um conjunto de encadeamentos personalizado para o fluxo paralelo do Java 8 ? Não consigo encontrá-lo em lugar algum.

Imagine que eu tenho um aplicativo de servidor e gostaria de usar fluxos paralelos. Mas o aplicativo é grande e multiencadeado, então eu quero compartimentá-lo. Eu não quero uma tarefa de execução lenta em um módulo das tarefas applicationblock de outro módulo.

Se não posso usar conjuntos de encadeamentos diferentes para módulos diferentes, significa que não posso usar fluxos paralelos com segurança na maioria das situações do mundo real.

Tente o seguinte exemplo. Existem algumas tarefas intensivas da CPU executadas em threads separados. As tarefas utilizam fluxos paralelos. A primeira tarefa é interrompida, portanto, cada etapa leva 1 segundo (simulado pela suspensão do encadeamento). O problema é que outros threads ficam presos e aguardam a conclusão da tarefa quebrada. Este é um exemplo artificial, mas imagine um aplicativo de servlet e alguém enviando uma tarefa de longa execução para o pool de junção de bifurcação compartilhada.

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

3
O que você quer dizer com pool de threads personalizado? Existe um único ForkJoinPool comum, mas você sempre pode criar seu próprio ForkJoinPool e enviar solicitações a ele.
edharned

7
Dica: O campeão de Java Heinz Kabutz inspeciona o mesmo problema, mas com impacto ainda pior: threads de deadlock do pool de junção de fork comum. Veja javaspecialists.eu/archive/Issue223.html
Peti

Respostas:


395

Na verdade, há um truque de como executar uma operação paralela em um pool de junção de forquilha específico. Se você executá-lo como uma tarefa em um pool de junção de bifurcação, ele permanece lá e não usa o comum.

final int parallelism = 4;
ForkJoinPool forkJoinPool = null;
try {
    forkJoinPool = new ForkJoinPool(parallelism);
    final List<Integer> primes = forkJoinPool.submit(() ->
        // Parallel task here, for example
        IntStream.range(1, 1_000_000).parallel()
                .filter(PrimesPrint::isPrime)
                .boxed().collect(Collectors.toList())
    ).get();
    System.out.println(primes);
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException(e);
} finally {
    if (forkJoinPool != null) {
        forkJoinPool.shutdown();
    }
}

O truque é baseado no ForkJoinTask.fork que especifica: "Organiza a execução assíncrona dessa tarefa no pool em que a tarefa atual está sendo executada, se aplicável, ou usando o ForkJoinPool.commonPool () se não for inForkJoinPool ()"


20
Os detalhes sobre a solução estão descritos aqui blog.krecan.net/2014/03/18/…
Lukas

3
Mas também está especificado que os fluxos usam o ForkJoinPoolou isso é um detalhe de implementação? Um link para a documentação seria bom.
Nicolai

6
@ Lucas Obrigado pelo trecho. Acrescentarei que a ForkJoinPoolinstância deve ser shutdown()quando não for mais necessária para evitar um vazamento de thread. (exemplo)
jck 18/02

5
Observe que há um erro no Java 8 que, embora as tarefas estejam sendo executadas em uma instância de pool customizado, elas ainda estão acopladas ao pool compartilhado: o tamanho da computação permanece proporcional ao pool comum e não ao pool customizado. Foi corrigido no Java 10: JDK-8190974
Terran

3
@terran Esse problema também foi corrigido no Java 8 bugs.openjdk.java.net/browse/JDK-8224620
Cutberto Ocampo

192

Os fluxos paralelos usam o padrão ForkJoinPool.commonPoolque, por padrão, possui menos um encadeamento como os processadores , conforme retornado por Runtime.getRuntime().availableProcessors()(Isso significa que os fluxos paralelos usam todos os seus processadores porque também usam o encadeamento principal):

Para aplicativos que exigem pools separados ou personalizados, um ForkJoinPool pode ser construído com um determinado nível de paralelismo de destino; por padrão, igual ao número de processadores disponíveis.

Isso também significa que se você tiver aninhado fluxos paralelos ou vários fluxos paralelos iniciados simultaneamente, todos compartilharão o mesmo pool. Vantagem: você nunca usará mais do que o padrão (número de processadores disponíveis). Desvantagem: você pode não ter "todos os processadores" atribuídos a cada fluxo paralelo iniciado (se houver mais de um). (Aparentemente, você pode usar um ManagedBlocker para contornar isso.)

Para alterar a maneira como os fluxos paralelos são executados, você pode

  • envie a execução do fluxo paralelo ao seu próprio ForkJoinPool: yourFJP.submit(() -> stream.parallel().forEach(soSomething)).get();ou
  • você pode alterar o tamanho do pool comum usando as propriedades do sistema: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20")para um paralelismo de destino de 20 threads. No entanto, isso não funciona mais após o patch suportado https://bugs.openjdk.java.net/browse/JDK-8190974 .

Exemplo deste último na minha máquina que possui 8 processadores. Se eu executar o seguinte programa:

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

A saída é:

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

Assim, você pode ver que o fluxo paralelo processa 8 itens por vez, ou seja, usa 8 threads. No entanto, se eu descomentar a linha comentada, a saída é:

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

Desta vez, o fluxo paralelo usou 20 threads e todos os 20 elementos no fluxo foram processados ​​simultaneamente.


30
Na commonPoolverdade, tem um a menos que availableProcessors, resultando em paralelismo total igual a availableProcessorsporque o encadeamento de chamada conta como um.
Marko Topolnik

2
enviar retorno ForkJoinTask. Para imitar parallel() get()é necessário:stream.parallel().forEach(soSomething)).get();
Grigory Kislin 23/10

5
Não estou convencido de que ForkJoinPool.submit(() -> stream.forEach(...))executará minhas ações do Stream com o dado ForkJoinPool. Eu esperaria que todo o Stream-Action fosse executado no ForJoinPool como uma ação, mas internamente ainda usando o ForkJoinPool padrão / comum. Onde você viu que o ForkJoinPool.submit () faria o que você diz que faz?
Frederic Leitenberger

@FredericLeitenberger Você provavelmente quis colocar seu comentário abaixo da resposta de Lukas.
assylias 23/01

2
Agora vejo que stackoverflow.com/a/34930831/1520422 mostra muito bem que realmente funciona como anunciado. Ainda não entendi como funciona. Mas estou bem com "funciona". Obrigado!
Frederic Leitenberger

39

Como alternativa ao truque de acionar a computação paralela dentro de seu próprio forkJoinPool, você também pode passar esse pool para o método CompletableFuture.supplyAsync, como em:

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

22

A solução original (configurando a propriedade paralelismo comum ForkJoinPool) não funciona mais. Observando os links na resposta original, uma atualização que quebra isso foi portada novamente para Java 8. Como mencionado nos encadeamentos vinculados, não foi garantido que esta solução funcionasse para sempre. Com base nisso, a solução é a solução forkjoinpool.submit with .get discutida na resposta aceita. Acho que o backport também corrige a falta de confiabilidade dessa solução.

ForkJoinPool fjpool = new ForkJoinPool(10);
System.out.println("stream.parallel");
IntStream range = IntStream.range(0, 20);
fjpool.submit(() -> range.parallel()
        .forEach((int theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();
System.out.println("list.parallelStream");
int [] array = IntStream.range(0, 20).toArray();
List<Integer> list = new ArrayList<>();
for (int theInt: array)
{
    list.add(theInt);
}
fjpool.submit(() -> list.parallelStream()
        .forEach((theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();

Não vejo a mudança no paralelismo quando vejo ForkJoinPool.commonPool().getParallelism()no modo de depuração.
d-coder

Obrigado. Fiz alguns testes / pesquisas e atualizei a resposta. Parece que uma atualização mudou, pois funciona em versões mais antigas.
Tod Casasent 13/06/19

Por que continuo recebendo isso: unreported exception InterruptedException; must be caught or declared to be thrownmesmo com todas as catchexceções no loop.
Rocky Li

Rocky, não estou vendo nenhum erro. Conhecer a versão Java e a linha exata ajudará. O "InterruptedException" sugere que a tentativa / captura em torno do sono não está fechada corretamente na sua versão.
Tod Casasent 7/08/19

13

Podemos alterar o paralelismo padrão usando a seguinte propriedade:

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

que pode ser configurado para usar mais paralelismo.


Embora seja um cenário global, que trabalha para aumentar a parallelStream
meadlai

Isso funcionou para mim na versão openjdk "1.8.0_222"
abbas

A mesma pessoa acima, isso não está funcionando para mim no openjdk "11.0.6"
abbas

8

Para medir o número real de threads usados, você pode verificar 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();

Isso pode produzir em uma CPU de 4 núcleos uma saída como:

5 // common pool
23 // custom pool

Sem .parallel()ele dá:

3 // common pool
4 // custom pool

6
O Thread.activeCount () não informa quais threads estão processando seu fluxo. Mapeie para Thread.currentThread (). GetName (), seguido por um distinto (). Então você perceberá que nem todos os threads no pool serão usados ​​... Adicione um atraso ao seu processamento e todos os threads no pool serão utilizados.
keyoxy 21/09/16

7

Até agora, usei as soluções descritas nas respostas desta pergunta. Agora, criei uma pequena biblioteca chamada Parallel Stream Support para isso:

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

Mas, como o @PabloMatiasGomez apontou nos comentários, existem desvantagens no mecanismo de divisão de fluxos paralelos, que depende muito do tamanho do pool comum. Consulte O fluxo paralelo de um HashSet não é executado em paralelo .

Estou usando esta solução apenas para ter pools separados para diferentes tipos de trabalho, mas não posso definir o tamanho do pool comum como 1, mesmo que não o use.



1

Tentei o ForkJoinPool personalizado da seguinte maneira para ajustar o tamanho do pool:

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

Aqui está a saída dizendo que o pool está usando mais threads do que o padrão 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]

Mas, na verdade, há um estranho , quando tentei obter o mesmo resultado usando ThreadPoolExecutoro seguinte:

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

mas eu falhei.

Ele só vai começar a parallelStream em uma nova linha e, em seguida, tudo o resto é apenas o mesmo, o que mais uma vez prova que o parallelStreamusará o ForkJoinPool para começar seus tópicos criança.


Qual poderia ser a possível razão para não permitir que outros executores?
omjego

@omjego Essa é uma boa pergunta, talvez você possa começar uma nova pergunta e fornecer mais detalhes para elaborar suas idéias;) #
Hearen

1

Vá para obter o AbacusUtil . O número do encadeamento pode ser especificado para o fluxo paralelo. Aqui está o código de exemplo:

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

Divulgação : Sou desenvolvedor do AbacusUtil.


1

Se você não deseja confiar em hacks de implementação, sempre há uma maneira de conseguir o mesmo, implementando coletores personalizados que combinarão mape collectsemântica ... e você não estaria limitado ao ForkJoinPool:

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

Felizmente, isso já foi feito aqui e está disponível no Maven Central: http://github.com/pivovarit/parallel-collectors

Disclaimer: Eu escrevi e assumo a responsabilidade.


0

Se você não se importa em usar uma biblioteca de terceiros, com o cyclops-react, você pode misturar Streams sequenciais e paralelos no mesmo pipeline e fornecer ForkJoinPools personalizados. Por exemplo

 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 se desejássemos continuar processando dentro de um fluxo sequencial

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

[Divulgação Sou o principal desenvolvedor do cyclops-react]


0

Se você não precisa de um ThreadPool personalizado, mas deseja limitar o número de tarefas simultâneas, pode usar:

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

(A pergunta duplicada solicitando isso está bloqueada, por favor, me carregue aqui)


-2

você pode tentar implementar esse ForkJoinWorkerThreadFactory e injetar na classe Fork-Join.

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }

você pode usar esse construtor de pool de junção de garfo para fazer isso.

notas: - 1. se você usar isso, leve em consideração que, com base na implementação de novos encadeamentos, o planejamento da JVM será afetado, que geralmente agenda encadeamentos de junção de forquilha para núcleos diferentes (tratados como um encadeamento computacional). 2. O agendamento de tarefas por junção forçada a threads não será afetado. 3. Ainda não descobri como o fluxo paralelo seleciona os segmentos da junção de forquilha (não foi possível encontrar a documentação adequada), então tente usar uma fábrica threadNaming diferente para garantir que os segmentos no fluxo paralelo estejam sendo selecionados de customThreadFactory que você fornece. 4. commonThreadPool não usará este customThreadFactory.


Você pode fornecer um exemplo utilizável que demonstraria como usar o que você especificou?
J. Murray
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.