Future.traverse似乎按顺序工作而不是并行工作。这是真的?

时间:2016-01-09 13:58:59

标签: scala

我的问题很简单,关于Future.traverse方法。 所以我有一个String-s列表。每个字符串都是网页的URL。然后我有一个类可以获取URL,加载网页并解析一些数据。所有这些都包含在Future {}中,因此异步处理结果。

课程简化如下:

class RatingRetriever(context:ExecutionContext) {
  def resolveFilmToRating(url:String):Future[Option[Double]]={
    Future{
      //here it creates Selenium web driver, loads the url and parses it.
    }(context)
  }
}

然后在另一个对象中我有这个:

    implicit val executionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2))
    .......
    val links:List[String] = films.map(film => film.asInstanceOf[WebElement].getAttribute("href"))
    val ratings: Future[List[Option[Double]]] = Future.traverse(links)(link => new RatingRetriever(executionContext).resolveFilmToRating(link))

当它工作时,我肯定可以看到它按顺序进行收集。如果我将执行上下文从固定大小池更改为单线程池,则行为是相同的。 所以我真的很想知道如何让Future.traverse并行工作。你能建议吗?

4 个答案:

答案 0 :(得分:6)

看看traverse的消息来源:

in.foldLeft(successful(cbf(in))) { (fr, a) => //we sequentially traverse Collection
  val fb = fn(a)                        //Your function comes here
  for (r <- fr; b <- fb) yield (r += b) //Just add elem to builder
}.map(_.result())                       //Getting the collection from builder

所以代码的并行程度取决于你的函数fn,看看两个例子:

1)此代码:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
object FutureTraverse extends App{
  def log(s: String) = println(s"${Thread.currentThread.getName}: $s")

  def withDelay(i: Int) = Future{
    log(s"withDelay($i)")
    Thread.sleep(1000)
    i
  }

  val seq = 0 to 10

  Future {
    for(i <- 0 to 5){
      log(".")
      Thread.sleep(1000)
    }
  }

  val resultSeq = Future.traverse(seq)(withDelay(_))

  Thread.sleep(6000)
}

有这样的输出:

ForkJoinPool-1-worker-5: .
ForkJoinPool-1-worker-3: withDelay(0)
ForkJoinPool-1-worker-1: withDelay(1)
ForkJoinPool-1-worker-7: withDelay(2)
ForkJoinPool-1-worker-5: .
ForkJoinPool-1-worker-3: withDelay(3)
ForkJoinPool-1-worker-1: withDelay(4)
ForkJoinPool-1-worker-7: withDelay(5)
ForkJoinPool-1-worker-5: .
ForkJoinPool-1-worker-3: withDelay(6)
ForkJoinPool-1-worker-1: withDelay(7)
ForkJoinPool-1-worker-7: withDelay(8)
ForkJoinPool-1-worker-5: .
ForkJoinPool-1-worker-3: withDelay(9)
ForkJoinPool-1-worker-1: withDelay(10)
ForkJoinPool-1-worker-5: .
ForkJoinPool-1-worker-5: .

2)只需更改withDelay函数:

  def withDelay(i: Int) = {
    Thread.sleep(1000)
    Future {
      log(s"withDelay($i)")
      i
    }
  }

并且您将获得顺序输出:

ForkJoinPool-1-worker-7: .
ForkJoinPool-1-worker-7: .
ForkJoinPool-1-worker-5: withDelay(0)
ForkJoinPool-1-worker-7: .
ForkJoinPool-1-worker-1: withDelay(1)
ForkJoinPool-1-worker-7: .
ForkJoinPool-1-worker-1: withDelay(2)
ForkJoinPool-1-worker-7: .
ForkJoinPool-1-worker-1: withDelay(3)
ForkJoinPool-1-worker-7: .
ForkJoinPool-1-worker-1: withDelay(4)
ForkJoinPool-1-worker-7: withDelay(5)
ForkJoinPool-1-worker-1: withDelay(6)
ForkJoinPool-1-worker-1: withDelay(7)
ForkJoinPool-1-worker-7: withDelay(8)
ForkJoinPool-1-worker-7: withDelay(9)
ForkJoinPool-1-worker-7: withDelay(10)

所以Future.traverse并不是一个并行的,它只是提交任务,它可以按顺序执行,整个并行的事情都在你提交的函数中。

答案 1 :(得分:4)

Scala的const getEvent = () => require('d3-selection').event; 确实并行工作。并行执行多少由Future.traverse确定!在下面,Scala ExecutionContext只计划Future上的任务。如果线程可用,则直接执行任务。否则,它将被安排在一个可用时运行。

有点难以看出java.util.concurrent.ExecutorService实现中的并行性来自

Future.traverse

但这里的诀窍是在 for--understanding之前定义def traverse(in: M[A])(fn: A => Future[B]) = in.foldLeft(successful(cbf(in))) { (fr, a) => val fb = fn(a) for (r <- fr; b <- fb) yield (r += b) }.map(_.result()) ! 通过执行fb函数并因此创建fn实例,此Future计划立即运行。 for-comprehension等待将来完成并将结果添加到累加器。

通过选择不同的Future

,可以很容易地看到它的并行性
ExecutionContext

当增加线程数时,函数将并行运行

val tp1 = java.concurrent.Executors.newFixedThreadPool(1)
implicit val ec = scala.concurrent.ExecutionContext.fromExecutorService(tp1)
Future.traverse((1 to 5)) { n => Future { sleep; println(n); n }}
1
2
3
4
5

答案 2 :(得分:0)

@nikiforo清楚,谢谢。关于我的特殊问题,如果我想让很少的浏览器同时工作,那么某种情况下,selenium web-driver希望每个实例都在一个单独的线程中实例化。所以我需要使用自定义Thread实现:

class FireFoxThread(r:Runnable) extends Thread(r:Runnable){
  val driver = new FirefoxDriver

  override def interrupt()={
    driver.quit
    super.interrupt
  }
}

然后从ThreadFactory实例化它:

val executorService:ExecutorService = Executors.newFixedThreadPool(3, new ThreadFactory {
      override def newThread(r: Runnable): Thread = new FireFoxThread(r)
})

这样我就可以在多个浏览器中处理我的网址。

答案 3 :(得分:0)

Future.traverse按顺序工作。它将TraversableOnce中的每个项目作为参数传递(在本例中为links),并使用您的映射函数创建未来。但是,它只会在前一个未来完成执行时创造这个未来,这将强制执行您已经看到的顺序行为。

您可以通过一个简单的代码示例清楚地看到这一点:

import scala.util.Random
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def sleep = Thread.sleep(100 + Random.nextInt(5000))

Future.traverse((1 to 100)){n => sleep; println(n); Future.successful(n)}

这将打印从1到100的数字,并且永远不会出现乱序。如果期货是并行执行的,随机睡眠将确保某些项目比之前发送的项目更早完成,但这并不会发生。

查看Future.traverse的来源,我们可以看到为什么会出现这种情况:

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

for (r <- fr; b <- fb)部分用于理解即将在您提供的未来上调用flatMap。一旦未来您的回调创建(fb)已完成执行,它就会被添加到结果列表中。这种情况在前一个未来(fr)完成之前不会发生,并且可以平面映射到其结果。

如果您想并行提交一组期货,可以使用Future.sequential

val retriver = new RatingRetriever(executionContext)
Future.sequence(links.map(link => retriver.resolveFilmToRating(link))

在这种情况下,您在links.map调用中创建了期货,因此它们都会立即开始执行。 Future.sequence执行将期货清单转换为结果列表的相对简单的工作。