Guia da biblioteca Java Parallel Collectors

Guia da biblioteca Java Parallel Collectors

1. Introdução

Parallel-collectors é uma pequena biblioteca que fornece um conjunto de coletores Java Stream API que permitem o processamento paralelo - ao mesmo tempo que contornam as principais deficiências dos fluxos paralelos padrão.

2. Dependências do Maven

Se quisermos começar a usar a biblioteca, precisamos adicionar uma única entrada no arquivopom.xml do Maven:


    com.pivovarit
    parallel-collectors
    1.1.0

Ou uma única linha no arquivo de compilação do Gradle:

compile 'com.pivovarit:parallel-collectors:1.1.0'

A versão mais recentecan be found on Maven Central.

3. Advertências sobre fluxos paralelos

Streams paralelos foram um dos destaques do Java 8, mas acabaram sendo aplicáveis ​​exclusivamente ao processamento pesado de CPU.

A razão para isso foi o fato de queParallel Streams were internally backed by a JVM-wide shared ForkJoinPool, which provided limited parallelisme foi usado por todos os fluxos paralelos em execução em uma única instância de JVM.

Por exemplo, imagine que temos uma lista de IDs e queremos usá-los para buscar uma lista de usuários e que essa operação é cara.

Poderíamos usar fluxos paralelos para isso:

List ids = Arrays.asList(1, 2, 3);
List results = ids.parallelStream()
  .map(i -> fetchById(i)) // each operation takes one second
  .collect(Collectors.toList());

System.out.println(results); // [user-1, user-2, user-3]

E, de fato, podemos ver que há uma aceleração perceptível. Mas isso se torna problemático se começarmos a executar várias operações de bloqueio paralelo ... em paralelo. This might quickly saturate the poole resultam em latências potencialmente enormes. É por isso que é importante construir anteparas criando pools de threads separados - para evitar que tarefas não relacionadas influenciem a execução umas das outras.

Para fornecer uma instânciaForkJoinPool personalizada, poderíamos aproveitarthe trick described here, mas essa abordagem dependia de um hack não documentado e estava com defeito até o JDK10. Podemos ler mais no próprio problema -[JDK8190974].

4. Coletores paralelos em ação

Coletores paralelos, como o nome sugere, são apenas coletores de API de fluxo padrão que permitem realizar operações adicionais em paralelo na fasecollect().

A classeParallelCollectors (que espelha a classeCollectors) é uma fachada que fornece acesso a toda a funcionalidade da biblioteca.

Se quiséssemos refazer o exemplo acima, poderíamos simplesmente escrever:

ExecutorService executor = Executors.newFixedThreadPool(10);

List ids = Arrays.asList(1, 2, 3);

CompletableFuture> results = ids.stream()
  .collect(ParallelCollectors.parallelToList(i -> fetchById(i), executor, 4));

System.out.println(results.join()); // [user-1, user-2, user-3]

O resultado é o mesmo, porém,we were able to provide our custom thread pool, specify our custom parallelism level, and the result arrived wrapped in a CompletableFuture instance without blocking the current thread. 

Streams paralelos padrão, por outro lado, não conseguiam atingir nenhum desses.

4.1. ParallelCollectors.parallelToList/ToSet()

Por mais intuitivo que seja, se quisermos processar umStream em paralelo e coletar os resultados em umList ouSet, podemos simplesmente usarParallelCollectors.parallelToList ouparallelToSet:

List ids = Arrays.asList(1, 2, 3);

List results = ids.stream()
  .collect(parallelToList(i -> fetchById(i), executor, 4))
  .join();

4.2. ParallelCollectors.parallelToMap()

Se quisermos coletar elementosStream em uma instânciaMap, assim como com a API Stream, precisamos fornecer dois mapeadores:

List ids = Arrays.asList(1, 2, 3);

Map results = ids.stream()
  .collect(parallelToMap(i -> i, i -> fetchById(i), executor, 4))
  .join(); // {1=user-1, 2=user-2, 3=user-3}

Também podemos fornecer uma instânciaMapSupplier personalizada:

Map results = ids.stream()
  .collect(parallelToMap(i -> i, i -> fetchById(i), TreeMap::new, executor, 4))
  .join();

E uma estratégia personalizada de resolução de conflitos:

List ids = Arrays.asList(1, 2, 3);

Map results = ids.stream()
  .collect(parallelToMap(i -> i, i -> fetchById(i), TreeMap::new, (s1, s2) -> s1, executor, 4))
  .join();

4.3. ParallelCollectors.parallelToCollection()

Da mesma forma que o anterior, podemos passar nossoCollection Supplier personalizado se quisermos obter resultados empacotados em nosso contêiner personalizado:

List results = ids.stream()
  .collect(parallelToCollection(i -> fetchById(i), LinkedList::new, executor, 4))
  .join();

4.4. ParallelCollectors.parallelToStream()

Se o acima não for suficiente, podemos realmente obter uma instânciaStream e continuar o processamento personalizado lá:

Map> results = ids.stream()
  .collect(parallelToStream(i -> fetchById(i), executor, 4))
  .thenApply(stream -> stream.collect(Collectors.groupingBy(i -> i.length())))
  .join();

4.5. ParallelCollectors.parallel()

Este nos permite transmitir resultados na ordem de conclusão:

ids.stream()
  .collect(parallel(i -> fetchByIdWithRandomDelay(i), executor, 4))
  .forEach(System.out::println);

// user-1
// user-3
// user-2

Nesse caso, podemos esperar que o coletor retorne resultados diferentes a cada vez, desde que introduzimos um atraso de processamento aleatório.

4.6. ParallelCollectors.parallelOrdered()

Esse recurso permite a transmissão de resultados exatamente como o descrito acima, mas mantém a ordem original:

ids.stream()
  .collect(parallelOrdered(i -> fetchByIdWithRandomDelay(i), executor, 4))
  .forEach(System.out::println);

// user-1
// user-2
// user-3

Nesse caso, o coletor sempre manterá a ordem, mas poderá ser mais lento que o acima.

5. Limitações

No momento da escrita,parallel-collectors don’t work with infinite streams mesmo se operações de curto-circuito forem usadas - é uma limitação de design imposta pelos internos da API de fluxo. Simplificando,Streams tratam os coletores como operações sem curto-circuito, portanto, o stream precisa processar todos os elementos upstream antes de ser encerrado.

A outra limitação é queshort-circuiting operations don’t interrupt the remaining tasks após o curto-circuito.

6. Conclusão

Vimos como a biblioteca de coletores paralelos nos permite realizar processamento paralelo usando a API Java StreamCollectorseCompletableFutures customizada para utilizar conjuntos de threads customizados, paralelismo e estilo sem bloqueio deCompletableFutures.

Como sempre, trechos de código estão disponíveisover on GitHub.

Para ler mais, consulte oparallel-collectors library no GitHub, oauthor’s blog e oTwitter account do autor.