如果可能,是否应该始终使用并行流?


514

使用Java 8和lambda,可以很容易地将集合作为流进行迭代,也很容易使用并行流。docs中的两个示例,第二个示例使用parallelStream:

myShapesCollection.stream()
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

myShapesCollection.parallelStream() // <-- This one uses parallel
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

只要我不关心顺序,使用并行会一直有益吗?有人会认为,更快地将工作划分到更多的内核上。

还有其他考虑事项吗?什么时候应该使用并行流,什么时候应该使用非并行流?

(问这个问题引发了关于如何以及何时使用并行流的讨论,不是因为我认为始终使用并行流是一个好主意。)

Answers:


735

与顺序流相比,并行流的开销要高得多。协调线程需要大量时间。我将默认使用顺序流,并且仅在以下情况下考虑并行流

  • 我要处理大量项目(或者每个项目的处理需要时间并且可以并行化)

  • 我首先遇到性能问题

  • 我尚未在多线程环境中运行该流程(例如:在Web容器中,如果我已经有许多并行处理的请求,则在每个请求中添加额外的并行度层可能会产生多于积极影响的负面影响)

在您的示例中,无论如何,性能都将受到对的同步访问的驱动System.out.println(),并且使此过程并行将没有效果,甚至没有效果。

此外,请记住,并行流并不能神奇地解决所有同步问题。如果在过程中使用的谓词和函数使用了共享资源,则必须确保所有内容都是线程安全的。尤其是副作用,如果并行使用,那么您真的要担心。

无论如何,不​​要猜测!只有度量会告诉您并行性是否值得。


18
好答案。我还要补充一点,如果您要处理大量项目,那只会增加线程协调问题;仅当每个项目的处理需要时间并且可并行化时,并行化才有用。
沃伦·露2014年

16
@WarrenDew我不同意。Fork / Join系统将简单地将N个项目分为例如4个部分,并依次处理这4个部分。然后将减少这4个结果。如果大量确实是巨大的,那么即使对于快速的单元处理,并行化也是有效的。但与往常一样,您必须进行衡量。
JB Nizet 2014年

我有一个对象集合,这些对象实现Runnable了调用start()它们以用作对象的方式Threads,是否可以将其更改为在.forEach()并行化中使用java 8流?然后,我将能够从类中剥离线程代码。但是有什么缺点吗?
ycomp

1
@JBNizet如果顺序处理4个部分,那么它是并行处理还是顺序知道没有区别吗?请澄清
-Harshana

3
@Harshana他显然意味着这4个部分中每个部分的元素都将被顺序处理。但是,零件本身可以同时处理。换句话说,如果您有多个可用的CPU内核,则每个部分可以独立于其他部分在其自己的内核上运行,同时依次处理其自身的元素。(注意:我不知道这是否是并行Java流的工作方式,我只是想弄清楚JBNizet的意思。)
明天

258

Stream API旨在简化计算方式,简化了计算方式,简化了顺序和并行之间的切换。

但是,仅仅因为它容易,并不意味着它总是一个好主意,实际上,仅仅因为可以就把它放到各处都是一个主意.parallel()

首先,请注意,并行化除了提供更多内核可用时更快执行的可能性外没有其他好处。并行执行总是比顺序执行涉及更多的工作,因为除了解决问题之外,它还必须执行子任务的调度和协调。希望您可以通过分解多个处理器上的工作来更快地找到答案。是否真的发生取决于很多事情,包括数据集的大小,每个元素要进行多少计算,计算的性质(具体来说,一个元素的处理是否与其他元素的处理相互作用?) ,可用处理器的数量以及与这些处理器竞争的其他任务的数量。

此外,请注意,并行性通常还会在计算中暴露出不确定性,而不确定性通常被顺序实现所隐藏;有时这无关紧要,或者可以通过限制所涉及的操作来缓解(即,归约运算符必须是无状态且具有关联性的)。

实际上,并行有时会加快您的计算速度,有时却不会,甚至有时会降低速度。最好先使用顺序执行进行开发,然后在其中应用并行性

(A)您知道提高性能实际上是有好处的,并且

(B)实际上会提供更高的性能。

(A)是业务问题,而不是技术问题。如果您是性能专家,通常可以查看代码并确定(B),但是明智的选择是衡量。(并且,甚至在您确信(A)之前都不要打扰;如果代码足够快,最好将您的大脑循环应用于其他地方。)

并行性的最简单性能模型是“ NQ”模型,其中N是元素数,Q是每个元素的计算量。通常,在开始获得性能优势之前,您需要产品NQ超过某个阈值。对于低Q问题,例如“从1到N的数字相加”,通常会看到N = 1000和N = 10000之间的收支平衡。对于较高Q的问题,您将在较低的阈值处看到收支平衡。

但是现实是相当复杂的。因此,在获得专家见识之前,请先确定顺序处理实际上何时使您付出了代价,然后衡量并行性是否会有所帮助。


18
这篇文章提供了有关NQ模型的更多详细信息:gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html
Pino

4
@specializt:将流从顺序流转换为并行流确实会更改算法(在大多数情况下)。这里提到的确定性是关于(任意)运算符可能依赖的属性(Stream实现不知道),但是当然不应该依赖。这就是该答案的这一部分试图说的话。如果您关心规则,就可以得到确定性的结果,就像您说的那样(否则并行流是完全没有用的),但是也有可能故意允许不确定性,例如使用findAny而不是findFirst
Holger

4
“首先,请注意,除了在有更多内核可用时更快执行的可能性之外,并行化没有任何其他好处”-或如果您要应用涉及IO的操作(例如 myListOfURLs.stream().map((url) -> downloadPage(url))...)。
Jules

6
@Pacerier这是一个很好的理论,但可悲的是天真(请参阅30年来尝试构建自动并行化编译器的历史)。由于在我们不可避免地犯错时,猜测足够多的时间来不惹恼用户是不切实际的,因此,负责任的事情就是让用户说出他们想要的内容。在大多数情况下,默认(顺序)是正确的,并且更可预测。
Brian Goetz

2
@Jules:切勿将并行流用于IO。它们仅用于CPU密集型操作。使用并行流,ForkJoinPool.commonPool()并且您不想阻止任务去那里。
R2C2 '18

68

我观看了Brian Goetz (Java语言架构师和Lambda Expressions的规范负责人)的演示之一。他详细解释了进行并行化之前要考虑的以下4点:

拆分/分解成本
–有时拆分比仅做工作要昂贵!
任务分配/管理成本
–可以花很多时间将工作交给另一个线程。
结果合并成本
–有时合并涉及复制大量数据。例如,增加数字很便宜,而合并集合很昂贵。
位置
-房间里的大象。这是每个人都可能错过的重要一点。您应该考虑缓存未命中,如果CPU由于缓存未命中而等待数据,那么并行化将不会带来任何好处。这就是为什么在缓存下一个索引(当前索引附近)时,基于数组的源能够并行处理最佳资源的原因,并且CPU遇到高速缓存未命中的可能性较小。

他还提到了确定并行加速机会的相对简单的公式。

NQ模型

N x Q > 10000

其中,
N =数据项的数量
Q =每个项目的工作量


13

JB撞到了头。我唯一可以添加的是Java 8不会进行纯并行处理,而是会进行后处理。是的,我写了这篇文章,并且从事F / J工作已经三十年了,所以我确实理解了这个问题。


10
流是不可迭代的,因为流是内部迭代而不是外部迭代。无论如何,这就是流的全部原因。如果您在学术工作方面遇到问题,那么函数式编程可能不适合您。函数式编程===数学===学术的。不,J8-FJ没有损坏,只是大多数人没有阅读f ******手册。Java文档非常清楚地说这不是并行执行框架。这就是所有分离器的全部原因。是的,它是学术性的,是的,如果您知道如何使用它,它将起作用。是的,使用自定义执行程序应该更容易
Kr0e 2014年

1
Stream确实具有iterator()方法,因此您可以根据需要在外部对其进行迭代。我的理解是他们没有实现Iterable,因为您只能使用一次该迭代器,而没有人可以决定是否可行。
Trejkaz 2015年

14
说实话:你的整篇文章读起来就像一个巨大的,复杂的咆哮-这几乎否定了它的信誉......我建议你用一个重新做不太积极的底色,否则没有多少人会真正懒得充分阅读......我只是萨彦
specializt

关于您的文章的几个问题……首先,为什么您显然将平衡树结构等同于有向无环图?是的,平衡树 DAG,但链表和除数组以外的几乎所有面向对象的数据结构也是如此。另外,当您说递归分解仅适用于平衡树结构,因此在商业上不相关时,您如何证明该断言的合理性?在我看来(诚然,没有真正深入研究这个问题),它应该可以工作在基于数组的数据结构(例如ArrayList/同样有效HashMap
Jules

1
该线程来自2013年,此后发生了很多变化。本部分仅用于评论而不是详细答案。
2016年

3

其他答案已经涵盖了性能分析以避免并行处理中的过早优化和开销成本。这个答案解释了并行流数据结构的理想选择。

作为一项规则,从并行性能提升是最好的流过ArrayListHashMapHashSet,和ConcurrentHashMap实例; 数组; int范围 和long范围。这些数据结构的共同之处在于,它们都可以准确而廉价地拆分为任意大小的子范围,这使得在并行线程之间划分工作变得容易。流库用于执行此任务的抽象方法是分隔符,该分隔符由spliteratoron Stream和方法返回Iterable

所有这些数据结构的共同点的另一个重要因素是,当按顺序进行处理时,它们提供了很好的引用局部性:顺序元素引用一起存储在内存中。这些引用所引用的对象在内存中可能彼此不接近,从而降低了引用的位置。事实证明,引用位置对于并行化批量操作至关重要:没有它,线程将花费大量时间空闲,等待数据从内存传输到处理器的缓存中。具有最佳引用位置的数据结构是原始数组,因为数据本身连续存储在内存中。

来源:第48项,并行使用流时要小心,有效的Java 3e作者:Joshua Bloch


2

永远不要将无限流与限制并行化。这是发生了什么:

    public static void main(String[] args) {
        // let's count to 1 in parallel
        System.out.println(
            IntStream.iterate(0, i -> i + 1)
                .parallel()
                .skip(1)
                .findFirst()
                .getAsInt());
    }

结果

    Exception in thread "main" java.lang.OutOfMemoryError
        at ...
        at java.base/java.util.stream.IntPipeline.findFirst(IntPipeline.java:528)
        at InfiniteTest.main(InfiniteTest.java:24)
    Caused by: java.lang.OutOfMemoryError: Java heap space
        at java.base/java.util.stream.SpinedBuffer$OfInt.newArray(SpinedBuffer.java:750)
        at ...

如果使用相同 .limit(...)

此处的说明: Java 8,在流中使用.parallel会导致OOM错误

同样,如果流是有序的并且具有比您要处理的元素更多的元素,请不要使用并行,例如

public static void main(String[] args) {
    // let's count to 1 in parallel
    System.out.println(
            IntStream.range(1, 1000_000_000)
                    .parallel()
                    .skip(100)
                    .findFirst()
                    .getAsInt());
}

这可能会运行更长的时间,因为并行线程可能会在大量数字范围内工作,而不是在关键的0-100范围内工作,这将导致很长的时间。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.