我有Iterable
个“工作单位”需要执行,没有特别的顺序,并且可以轻松并行运行而不会相互干扰。
不幸的是,一次运行太多的内存会超出我的可用RAM,因此我需要确保在任何给定时间只有少数同时运行。
最基本的,我想要一个这种类型签名的功能:
parMap[A, B](xs: Iterator[A], f: A => B, chunkSize: Int): Iterator[B]
这样输出Iterator
不一定与输入的顺序相同(如果我想保持结果来自何处的知识,我可以输出一对输入或其他东西。)消费者然后可以逐步消耗生成的迭代器,而不会占用机器的所有内存,同时保持尽可能多的并行性。
此外,我希望功能尽可能高效。我最初的想法就是按照以下方式做一些事情:
xs.iterator.grouped(chunkSize).flatMap(_.toSet.par.map(f).iterator)
我希望toSet
能告知Scala的并行集合,它可以在任何时候准备好它的迭代器就可以开始生成元素,并且grouped
调用是为了限制同时工人数量。不幸的是,toSet
调用似乎没有达到预期的效果(结果的返回顺序与没有par
调用的情况相同,在我的实验中)和{ {1}}通话不是最理想的。例如,如果我们的组大小为100,并且其中99个作业立即在十几个核心上完成,但其中一个特别慢,那么大多数剩余核心将处于空闲状态,直到我们可以移动到下一个组。拥有一个最大程度上与我的块大小一样大的“自适应窗口”会更加清晰,但不会被缓慢的工作者所阻碍。
我可以设想用自己的工作窃取(de)队列或类似的东西写这样的东西,但我想在处理并发原语的许多艰苦工作已经在某种程度上为我完成了在Scala的并行集合库中。有谁知道我可以重用哪些部分来构建这个功能,或者对如何实现这样的操作有其他建议?
答案 0 :(得分:3)
Parallel collections框架允许您指定给定任务使用的最大线程数。使用scala-2.10,你想做:
def parMap[A,B](x : Iterable[A], f : A => B, chunkSize : Int) = {
val px = x.par
px.tasksupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(chunkSize))
px map f
}
这将阻止任何时候运行超过chunkSize
个操作。这使用了下面的工作窃取策略来保持演员的工作,因此不会遇到与上面的grouped
示例相同的问题。
然而,这样做不会将结果重新排序为首先完成的顺序。为此,我建议将操作转变为演员并让一个小演员池运行操作,然后在完成时将结果发回给你。