为什么当 .stream().parallel() 执行相同的操作时,Collection.parallelStream() 存在?

2022-08-31 22:09:02

在 Java 8 中,Collection 接口通过两个方法进行了扩展,这两个方法返回 :,返回顺序流,和 返回可能并行的流。流本身还有一个返回等效并行流的方法(将当前流变异为并行流或创建新流)。Stream<E>stream()parallelStream()parallel()

重复有明显的缺点:

  • 这令人困惑。一个问题是,调用两个parallelStream().parallel()是否有必要确保流是并行的,因为paralledStream()可能返回顺序流。如果parallelStream()不能保证,为什么它存在?另一种方式也令人困惑 - 如果parallelStream()返回一个顺序流,可能有一个原因(例如,一个固有的顺序数据结构,其中并行流是一个性能陷阱);Stream.parallel() 应该为这样的流做些什么?(不受支持的操作异常是并行()的规范所不允许的。

  • 如果现有实现具有具有不兼容的返回类型的名称类似的方法,则向接口添加方法存在冲突风险。除了 stream() 之外,添加 parallelStream() 会使风险加倍,而收益微乎其微。(请注意,parallelStream() 在某一时刻只是被命名为 parallel(),尽管我不知道它是为了避免名称冲突还是出于其他原因而重命名的。

为什么在调用 Collection.stream().parallel() 做同样的事情时,Collection.parallelStream() 存在?


答案 1

Javadocs本身并没有回答这个问题,所以它被送到邮件列表上寻找基本原理。我浏览了 lambda-libs-spec-observers 存档,发现了一个专门关于 Collection.parallelStream() 的线程,另一个线程涉及 java.util.Arrays 是否应该提供 parallelStream() 来匹配(或者实际上,是否应该删除它)。没有一劳永逸的结论,所以也许我从另一个清单中遗漏了一些东西,或者这个问题在私下讨论中得到了解决。(也许这次讨论的主要负责人之一布莱恩·戈茨(Brian Goetz)可以填补任何遗漏的东西。Collection.(parallelS|s)tream()Stream

参与者很好地表达了他们的观点,所以这个答案主要只是相关引文的组织,在[括号]中进行了一些澄清,按重要性顺序呈现(正如我所解释的那样)。

parallelStream() 涵盖了一个非常常见的情况

Brian Goetz 在第一个线程中解释了为什么即使在其他并行流工厂方法被删除后,也有足够的价值来保留:Collections.parallelStream()

我们没有这些[流工厂]的显式并行版本;我们最初这样做了,为了修剪API表面积,我们削减了它们,理论上从API中删除20多种方法值得在表面怪异和性能成本之间进行权衡。但是我们没有在Collection上做出这样的选择。.intRange(...).parallel()

我们可以删除 ,也可以添加所有生成器的并行版本,或者我们可以什么都不做,保持原样。我认为在API设计的基础上,所有这些都是合理的。Collection.parallelStream()

我有点喜欢现状,尽管它不一致。我们没有2N流构造方法,而是N + 1 - 但是额外的1涵盖了大量情况,因为它被每个集合继承。因此,我可以向自己证明为什么拥有额外的1种方法是值得的,以及为什么接受不进一步的不一致是可以接受的。

其他人不同意吗?N+1 [Collections.parallelStream() only] 是这里实际的选择吗?或者我们应该追求N的纯度[依靠Stream.parallel()]?还是2N的便利性和一致性[所有工厂的并行版本]?或者有没有一些更好的N+3 [Collections.parallelStream()加上其他特殊情况],对于其他一些特别选择的情况,我们想要给予特别支持?

Brian Goetz在后来的讨论中坚持这一立场:Arrays.parallelStream()

我仍然非常喜欢Collection.parallelStream;它具有巨大的可发现性优势,并在API表面积上提供了相当大的回报 - 这是另一种方法,但在很多地方提供了价值,因为Collection将是流源的一个非常常见的情况。

parallelStream() 性能更高

布莱恩·戈茨

直接版本 [parallelStream()] 的性能更高,因为它需要较少的包装(要将流转换为并行流,您必须首先创建顺序流,然后将其状态的所有权转移到新的流中。

在回应Kevin Bourrillion对效果是否显着的怀疑时,Brian再次指出

取决于你算了多少。Doug 在并行操作的途中计算了单个对象的创建和虚拟调用,因为在你开始分叉之前,你就站在了阿姆达尔定律的错误一边——这都是在你分叉任何工作之前发生的“串行分数”,这会把你的盈亏平衡阈值进一步推开。因此,快速获得并行操作的设置路径很有价值。

Doug Lea跟进,但对冲了他的立场:

处理并行库支持的人需要对这些事情进行一些态度调整。在即将成为典型机器的机器上,您浪费在设置并行性时浪费的每个周期的成本都是64个周期。如果需要创建 64 个对象才能启动并行计算,您可能会有不同的反应。

也就是说,我总是完全支持为了更好的API而迫使实现者更加努力地工作,只要API不排除有效的实现。因此,如果杀戮真的很重要,我们会找到一些方法来变成一个有点翻转或类似的东西。parallelStreamstream().parallel()

事实上,后面的讨论注意到了较低的Stream.parallel()成本Arrays.parallelStream()

stream().parallel() stateful 使未来复杂化

在讨论时,将流从顺序切换到并行和向后切换可以与其他流操作交错。Brian Goetz代表Doug Lea解释了为什么顺序/并行模式切换可能会使Java平台的未来发展复杂化:

我将尽我最大的努力来解释为什么:因为它(就像你不喜欢的有状态方法(排序,不同,限制)一样,使我们越来越远离能够以传统的数据并行结构来表达流管道,这进一步限制了我们直接将它们映射到明天的计算基板的能力,无论是矢量处理器, FPGA,GPU或我们烹饪的任何东西。

滤波-映射-减少映射非常清晰地映射到各种并行计算基板;filter-parallel-map-sequential-sorted-limit-parallel-map-uniq-reduce 不会。

因此,这里的整个API设计体现了许多紧张关系,两者之间是使用户可能想要表达的内容变得容易,而这样做是以一种我们可以预测的方式,通过透明的成本模型快速完成。

经过进一步讨论后,此模式切换已被删除。在当前版本的库中,流管道是顺序的或并行的。最后一次调用 /获胜。除了回避有状态性问题之外,此更改还提高了使用从顺序流工厂设置并行管道的性能。sequential()parallel()parallel()

将parallelStream()公开为一等公民,可以提高程序员对库的看法,从而使他们能够编写更好的代码

Brian Goetz再次回应了Tim Peierls的论点,即允许程序员在并行之前按顺序理解流:Stream.parallel()

我对这种顺序直觉的价值有一个稍微不同的观点 - 我认为普遍的“顺序期望”是整个努力的最大挑战之一;人们不断带来他们不正确的顺序偏差,这导致他们做一些愚蠢的事情,比如使用单元素数组作为“欺骗”“愚蠢”编译器的一种方式,让他们捕获可变的局部,或者使用lambda作为参数来映射将在计算期间使用的突变状态(以非线程安全的方式), 然后,当它指出他们正在做的事情时,耸耸肩并说“是的,但我没有并行不做。

我们在合并顺序流和并行流时进行了许多设计权衡。我相信,结果是一个干净的,并且会增加图书馆在10年多年后仍然有用的机会,但我特别不喜欢鼓励人们认为这是一个顺序库的想法,侧面钉着一些平行的袋子。


答案 2

推荐