parallel flatMap始终是顺序的

时间:2017-07-11 14:58:32

标签: java java-8 java-stream java-9

假设我有这段代码:

 Collections.singletonList(10)
            .parallelStream() // .stream() - nothing changes
            .flatMap(x -> Stream.iterate(0, i -> i + 1)
                    .limit(x)
                    .parallel()
                    .peek(m -> {
                        System.out.println(Thread.currentThread().getName());
                    }))
            .collect(Collectors.toSet());

输出是相同的线程名称,所以这里没有parallel的好处 - 我的意思是有一个线程可以完成所有的工作。

flatMap内,有以下代码:

result.sequential().forEach(downstream);

我理解强制sequential属性,如果"外部"流将是并行的(它们可能会阻塞),"外部"将不得不等待" flatMap"完成和反过来(因为使用相同的公共池)但为什么总是强迫它?

可能在以后的版本中发生变化吗?

2 个答案:

答案 0 :(得分:9)

有两个不同的方面。

首先,只有一个管道是顺序的或并行的。在内部流中选择顺序或并行是无关紧要的。请注意,您在引用的代码段中看到的downstream消费者代表整个后续流管道,因此在您的代码中,以.collect(Collectors.toSet());结尾,此消费者最终会将结果元素添加到单个{{1不是线程安全的实例。因此,与该单个消费者并行处理内部流将破坏整个操作。

如果外部流被拆分,那么引用的代码可能会与不同的消费者同时调用,从而添加到不同的集合中。这些调用中的每一个都将处理外部流映射到不同内部流实例的不同元素。由于外部流仅由单个元素组成,因此无法拆分。

这方法已经实现,也是Why filter() after flatMap() is “not completely” lazy in Java streams?问题的原因,因为在内部流上调用Set会将所有元素传递给下游消费者。正如this answer所示,支持懒惰和子流分裂的替代实现是可能的。但这是实现它的一种根本不同的方式。 Stream实现的当前设计主要由消费者构成工作,因此最后,源分裂器(以及从它分离的那些)接收forEach代表Consumer或{中的整个流管道{1}}。相比之下,链接答案的解决方案会进行分裂器组合,从而产生新的tryAdvance委托给分裂者。我认为,这两种方法都有优势,我不确定,在反过来工作时,OpenJDK实施会失去多少。

答案 1 :(得分:2)

对于像我这样迫切需要并行化 flatMap 并且需要一些实用解决方案的人,不仅仅是历史和理论。

我想出的最简单的解决方案是手动进行展平,基本上是将其替换为 map + reduce(Stream::concat)

以下是演示如何执行此操作的示例:

@Test
void testParallelStream_NOT_WORKING() throws InterruptedException, ExecutionException {
    new ForkJoinPool(10).submit(() -> {
        Stream.iterate(0, i -> i + 1).limit(2)
                .parallel()

                // does not parallelize nested streams
                .flatMap(i -> generateRangeParallel(i, 100))

                .peek(i -> System.out.println(currentThread().getName() + " : generated value: i=" + i))
                .forEachOrdered(i -> System.out.println(currentThread().getName() + " : received value: i=" + i));
    }).get();
    System.out.println("done");
}

@Test
void testParallelStream_WORKING() throws InterruptedException, ExecutionException {
    new ForkJoinPool(10).submit(() -> {
        Stream.iterate(0, i -> i + 1).limit(2)
                .parallel()

                // concatenation of nested streams instead of flatMap, parallelizes ALL the items
                .map(i -> generateRangeParallel(i, 100))
                .reduce(Stream::concat).orElse(Stream.empty())

                .peek(i -> System.out.println(currentThread().getName() + " : generated value: i=" + i))
                .forEachOrdered(i -> System.out.println(currentThread().getName() + " : received value: i=" + i));
    }).get();
    System.out.println("done");
}

Stream<Integer> generateRangeParallel(int start, int num) {
    return Stream.iterate(start, i -> i + 1).limit(num).parallel();
}

// run this method with produced output to see how work was distributed
void countThreads(String strOut) {
    var res = Arrays.stream(strOut.split("\n"))
            .map(line -> line.split("\\s+"))
            .collect(Collectors.groupingBy(s -> s[0], Collectors.counting()));
    System.out.println(res);
    System.out.println("threads  : " + res.keySet().size());
    System.out.println("work     : " + res.values());
}

在我的机器上运行的统计数据:

NOT_WORKING case stats:
{ForkJoinPool-1-worker-23=100, ForkJoinPool-1-worker-5=300}
threads  : 2
work     : [100, 300]

WORKING case stats:
{ForkJoinPool-1-worker-9=16, ForkJoinPool-1-worker-23=20, ForkJoinPool-1-worker-21=36, ForkJoinPool-1-worker-31=17, ForkJoinPool-1-worker-27=177, ForkJoinPool-1-worker-13=17, ForkJoinPool-1-worker-5=21, ForkJoinPool-1-worker-19=8, ForkJoinPool-1-worker-17=21, ForkJoinPool-1-worker-3=67}
threads  : 10
work     : [16, 20, 36, 17, 177, 17, 21, 8, 21, 67]