让我们多次运行以下代码行:
Set(1,2,3,4,5,6,7).par.fold(0)(_ - _)
结果非常有趣:
scala> Set(1,2,3,4,5,6,7).par.fold(0)(_ - _)
res10: Int = 8
scala> Set(1,2,3,4,5,6,7).par.fold(0)(_ - _)
res11: Int = 20
但显然它应该像顺序版本一样:
scala> Set(1,2,3,4,5,6,7).fold(0)(_ - _)
res15: Int = -28
我理解操作-
在整数上是非关联的,这就是这种行为背后的原因,但我的问题很简单:这不是说fold
不应该在.par
中并行化{1}}集合的实现?
答案 0 :(得分:7)
当您查看standard library documentation时,您会发现fold
在这里不确定:
使用指定的关联二元运算符折叠此序列的元素。 对元素执行操作的顺序是未指定的,可能是不确定的。
作为替代方案,有foldLeft
:
将二元运算符应用于起始值以及此序列的所有元素,从左到右。 将二元运算符应用于起始值以及此集合或迭代器的所有元素,从左到右。
注意:可能会为不同的运行返回不同的结果,除非基础集合类型是有序的或运算符是关联的和可交换的。
由于Set
不是有序集合,因此没有可以折叠元素的规范顺序,因此即使对于foldLeft
,标准库也允许自身不确定。如果你在这里使用有序序列,foldLeft
在这种情况下将是确定性的。
答案 1 :(得分:4)
scaladoc 确实说:
元素减少的顺序是未指定的,可能是不确定的。
因此,如您所述,在ParSet#fold
中应用的非关联的二元运算不能保证产生确定性结果。上面的文字是警告就是你得到的。
这是否意味着ParSet#fold
(和表兄弟)不应该并行化?不完全是。如果您的二元操作是可交换的并且您不关心副作用的非确定性(不是fold
应该有任何),那么就没有了#ta; ta问题。然而,你需要谨慎对待平行的收藏品。
是否正确更多的是意见问题。有人可能会争辩说,如果一种方法可能导致意外的非确定性,那么它就不应该存在于语言或库中。但另一种方法是剪切掉功能,以便ParSet
缺少大多数其他集合实现中存在的功能。您可以使用相同的思路来建议删除Stream#foreach
以防止人们意外触发无限流上的无限循环,但是你应该这样做吗?
答案 2 :(得分:1)
将fold
操作与高工作负载并行化非常有用,但是,为了保证调用collection.par.fold(z)(f)
的确定性输出,必须满足以下条件:
1- f(f(a,b),c) == f(a,f(b,c)) // Associativity
2- f(z,a) == f(a,z) == a
,其中z
是f
的中性元素(如0表示总和,1表示乘法)。
Fabian的答案建议改为使用foldLeft
。尽管这是确定性的,但结合使用.par
并不会真正并行化任何内容。因为foldLeft
本质上是连续的。