并行集合中scala折叠的行为

时间:2016-08-03 03:44:07

标签: scala parallel-processing

让我们多次运行以下代码行:

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}}集合的实现?

3 个答案:

答案 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,其中zf的中性元素(如0表示总和,1表示乘法)。

Fabian的答案建议改为使用foldLeft。尽管这是确定性的,但结合使用.par并不会真正并行化任何内容。因为foldLeft本质上是连续的。