![](/img/trans.png)
[英]Scala: Why is this Future.traverse slow? The body is not running in parallel
[英]Future.traverse seems to work sequentially and not in parallel. Is this true?
我的问题很简单,关于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并行工作。 你能建议吗?
看看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不一定是并行的,它只是提交任务,它可以按顺序执行,整个并行的东西都在你提交的函数中。
Scala的Future.traverse
确实可以并行工作。 并行执行多少是由ExecutionContext
确定的! 在下面,Scala Future
只是在java.util.concurrent.ExecutorService
上安排任务。 如果线程可用,则直接执行任务。 否则,它将被安排在一个可用时运行。
在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-comprehension 之前定义fb
! 通过执行fn
函数并因此创建Future
实例,此Future计划立即运行。 for-comprehension等待将来完成并将结果添加到累加器。
通过选择不同的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
增加线程数时,函数将并行运行
import scala.util.Random
import scala.concurrent.Future
def sleep = Thread.sleep(100 + Random.nextInt(1000))
val tp5 = java.util.concurrent.Executors.newFixedThreadPool(5)
implicit val ec = scala.concurrent.ExecutionContext.fromExecutorService(tp5)
Future.traverse((1 to 5)) { n => Future { sleep; println(n); n }}
3
2
4
5
1
@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)
})
这样我就可以在多个浏览器中处理我的URL。
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
)完成后才会发生这种情况,并且可以将flatMap映射到其结果。
如果要并行提交一组期货,可以使用Future.sequential
:
val retriver = new RatingRetriever(executionContext)
Future.sequence(links.map(link => retriver.resolveFilmToRating(link))
在这种情况下,您将在links.map
调用中创建期货,因此它们都会立即开始执行。 Future.sequence
完成了将期货列表转换为结果列表的相对简单的工作。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.