是否存在"缓慢" Future.traverse版本?

时间:2015-02-14 10:23:34

标签: scala future

我发现为一个用户请求构建大量期货通常是一种不好的做法。这些期货可以填补执行上下文,这将影响其他请求。这不太可能是你真正想要的。保持期货数量很小很简单 - 仅在for-comprehensions中创建新期货,使用flatMap等。但有时可能需要为每个Seq项目创建Future。使用Future.sequence或Future.traverse导致上述问题。所以我最终得到了这个解决方案,它不会同时为每个收集项创建Futures:

  def ftraverse[A, B](xs: Seq[A])(f: A => Future[B])(implicit ec: ExecutionContext): Future[Seq[B]] = {
    if(xs.isEmpty) Future successful Seq.empty[B]
    else f(xs.head) flatMap { fh => ftraverse(xs.tail)(f) map (r => fh +: r) }
  }

我想知道,也许我正在发明一个轮子,实际上这个功能已经存在于Scala的标准库中?另外我想知道,你遇到过描述的问题,你是如何解决的?也许,如果这是Futures的一个众所周知的问题,我应该在Future.scala中创建一个pull请求,这样这个函数(或者它的更通用版本)会被包含在标准库中吗?

UPD:更通用的版本,有限的并行性:

  def ftraverse[A, B](xs: Seq[A], chunkSize: Int, maxChunks: Int)(f: A => Future[B])(implicit ec: ExecutionContext): Future[Seq[B]] = {
    val xss = xs.grouped(chunkSize).toList
    val chunks = xss.take(maxChunks-1) :+ xss.drop(maxChunks-1).flatten
    Future.sequence{ chunks.map(chunk => ftraverse(chunk)(f) ) } map { _.flatten }
  } 

4 个答案:

答案 0 :(得分:12)

不,标准库中没有这样的内容。是否应该是否,我不能说。我不认为想要按照严格的顺序执行Future是很常见的。但是当你想要的时候,你可以很容易地实现自己的方法。我个人只是为了这个目的在我自己的库中保留一个方法。但是,使用标准库有一种方法可以很方便。如果,它应该更通用。

修改当前traverse以按顺序处理Future实际上非常简单,而不是并行处理。这是current version,它使用foldLeft而不是递归:

def traverse[A, B, M[X] <: TraversableOnce[X]](in: M[A])(fn: A => Future[B])(implicit cbf: CanBuildFrom[M[A], B, M[B]], executor: ExecutionContext): Future[M[B]] =
    in.foldLeft(Future.successful(cbf(in))) { (fr, a) =>
      val fb = fn(a)
      for (r <- fr; b <- fb) yield (r += b)
    }.map(_.result())

Future之前通过分配flatMap(之后执行)创建val fb = fn(a)。所有人需要做的是在fn(a)内移动flatMap以延迟在集合中创建后续Future

def traverseSeq[A, B, M[X] <: TraversableOnce[X]](in: M[A])(fn: A => Future[B])(implicit cbf: CanBuildFrom[M[A], B, M[B]], executor: ExecutionContext): Future[M[B]] =
    in.foldLeft(Future.successful(cbf(in))) { (fr, a) =>
      for (r <- fr; b <- fn(a)) yield (r += b)
    }.map(_.result())

另一种限制执行大量Future的影响的方法是使用不同的ExecutionContext。例如,在Web应用程序中,我可能会为数据库调用保留一个ExecutionContext,一个用于调用Amazon S3,另一个用于缓慢的数据库调用。

一个非常简单的实现可以使用固定的线程池:

import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext
val executorService = Executors.newFixedThreadPool(4)
val executionContext = ExecutionContext.fromExecutorService(executorService)

此处执行的大量Future将填充ExecutionContext,但这会阻止它们填充其他上下文。

如果您正在使用Akka,则可以使用ExecutionContext中的Dispatchers轻松地从配置创建ActorSystem

my-dispatcher {
  type = Dispatcher
  executor = "fork-join-executor"
  fork-join-executor {
    parallelism-min = 2
    parallelism-factor = 2.0
    parallelism-max = 10
  }
  throughput = 100
}

如果您有一个名为ActorSystem的{​​{1}},则可以通过以下方式访问它:

system

所有这些都取决于您的使用案例。虽然我将异步计算分成不同的上下文,但有时候我仍然希望implicit val executionContext = system.dispatchers.lookup("my-dispatcher") 顺序地平滑这些上下文的使用。

答案 1 :(得分:4)

您的问题似乎与您创建的期货数量无关,但与其执行的公平性无关。考虑如何处理期货(mapflatMaponCompletefold等)的回调:它们被放置在执行者的队列中并在其结果被执行时执行父母期货已经完成。

如果你所有的期货共享同一个执行者(即队列),他们确实会像你说的那样互相辱骂。解决这个公平问题的常用方法是使用Akka演员。对于每个请求,启动一个新的actor(具有自己的队列)并让该类型的所有actor 共享一个ExecutionContext。在使用ExecutionContext配置属性转移到另一个共享throughput的actor之前,您可以限制actor执行的最大消息数。

答案 2 :(得分:0)

这不是并行集合的用途吗?

val parArray = (1 to 1000000).toArray.par
sum = parArray.map(_ + _)
res0: Int = 1784293664

看起来像普通的同步方法调用,但并行集合将使用线程池并行计算地图(竞争条件!)。您可以在此处找到更多详细信息:http://docs.scala-lang.org/overviews/parallel-collections/overview.html

答案 3 :(得分:0)

假设期货的创建不是那么精细以至于开销过高(在这种情况下建议使用并行集合的答案可能是最有用的),你可以创建一个不同的,隐式定义的执行期货在其下运行的上下文由不同的执行者以及它自己的线程支持。

您可以致电ExecutionContext.fromExecutorServiceExecutionContext.fromExecutor来执行此操作。