Scala中的带有期货的异步IO


68

假设我要从一些URL下载一个(可能很大)图像列表。我正在使用Scala,所以我要做的是:

import scala.actors.Futures._

// Retrieve URLs from somewhere
val urls: List[String] = ...

// Download image (blocking operation)
val fimages: List[Future[...]] = urls.map (url => future { download url })

// Do something (display) when complete
fimages.foreach (_.foreach (display _))

我对Scala有点陌生,所以对我来说,这仍然有点像魔术:

  • 这是正确的方法吗?如果不是,还有其他选择吗?
  • 如果我要下载100张图像,这会一次创建100个线程,还是会使用线程池?
  • 最后一条指令(display _)是否会在主线程上执行,否则,如何确定?

谢谢你的建议!

Answers:


133

在Scala 2.10中使用期货。他们是Scala团队,Akka团队和Twitter之间的共同工作,以期实现更加标准化的未来API和实现,以供跨框架使用。我们刚刚在以下网站发布了指南:http//docs.scala-lang.org/overviews/core/futures.html

除了完全非阻塞(默认情况下,尽管我们提供了执行托管阻塞操作的能力)和可组合性之外,Scala的2.10期货还带有隐式线程池来执行任务,以及一些用于管理超时的实用程序。

import scala.concurrent.{future, blocking, Future, Await, ExecutionContext.Implicits.global}
import scala.concurrent.duration._

// Retrieve URLs from somewhere
val urls: List[String] = ...

// Download image (blocking operation)
val imagesFuts: List[Future[...]] = urls.map {
  url => future { blocking { download url } }
}

// Do something (display) when complete
val futImages: Future[List[...]] = Future.sequence(imagesFuts)
Await.result(futImages, 10 seconds).foreach(display)

上面,我们首先导入一些东西:

  • future:用于创建未来的API。
  • blocking:用于托管阻止的API。
  • Future:期货伴随对象,其中包含许多有用的期货收集方法。
  • Await:用于阻止将来的单例对象(将其结果传输到当前线程)。
  • ExecutionContext.Implicits.global:默认的全局线程池,一个ForkJoin池。
  • duration._:用于管理超时时间的实用程序。

imagesFuts仍与您最初所做的大致相同-唯一的区别是我们使用了托管阻止- blocking。它通知线程池您传递给它的代码块包含长时间运行或阻塞的操作。这允许池临时产生新的工作程序,以确保所有工作程序都不会被阻塞。这样做是为了防止阻塞应用程序中的饥饿(锁定线程池)。请注意,线程池还知道托管阻塞块中的代码何时完成-因此它将在该点删除备用工作线程,这意味着该池将缩小到其预期大小。

(如果要绝对防止创建其他线程,则应使用AsyncIO库,例如Java的NIO库。)

然后,我们用未来的伴侣要转换的对象的收集方法imagesFuts,从 List[Future[...]]Future[List[...]]

Await对象是我们如何确保display在调用Await.result线程上执行的对象,只需强制当前线程等待,直到将来通过它为止即可。(这在内部使用托管阻止。)


感谢您的深入回答!如果我理解正确,如果您未指定“阻塞”,那么线程池可能会耗尽工作线程,并且如果每个工作线程都无限期地忙碌,则会永远阻塞线程池?另外,我是否可以创建自己的实例ExecutionContext来强制完成回调(而不是实际的后台进程)在特定线程(即UI线程,使用特定于框架的方法)上异步执行?
FX

从技术上讲:不惜一切代价避免阻塞。仅当您别无选择时才进行阻止。
维克多·巴生

1
但是要回答您先前的问题-是的,如果所有线程都阻塞并且您不使用托管阻塞,则默认FJPool可能会用完工作线程。是ExecutionContextinvokeLater,例如,您可以使用Swing创建自己的,并将其显式传递给foreachon,futImages而不是使用Await.result
Heather Miller

2
背后的东西实际上是如何blocking工作的?它具有自己的线程池,还是在我们通过提交任务时仅创建新线程blocking
maks 2013年

4
您为什么在其中使用阻止功能url => future { blocking { download url } },为什么不使用公正url => future { download url }呢?
アレックス

5
val all = Future.traverse(urls){ url =>
  val f = future(download url) /*(downloadContext)*/
  f.onComplete(display)(displayContext)
  f
}
Await.result(all, ...)
  1. 在2.10(现在是RC)中使用scala.concurrent.Future。
  2. 使用隐式的ExecutionContext
  3. 新的Future文档明确指出,如果该值可用,onComplete(和foreach)可以立即进行评估。老演员Future也做同样的事情。根据显示要求,可以提供合适的ExecutionContext(例如,单线程执行程序)。如果只希望主线程等待加载完成,则遍历为您提供了等待的机会。

3
  1. 是的,对我来说似乎很好,但是您可能想研究更强大的twitter-utilAkka Future API(Scala 2.10将具有这种样式的新Future库)。

  2. 它使用线程池。

  3. 不,不会。为此,您需要使用GUI工具包的标准机制(SwingUtilities.invokeLater对于Swing或Display.asyncExecSWT)。例如

    fimages.foreach (_.foreach(im => SwingUtilities.invokeLater(new Runnable { display im })))
    

感谢您的回答,我很高兴知道我的方法是明智的!我实际上是在尝试Scala for Android,因此与可怕的Java语法相比,它会派上用场!
FX

关于#3,我在写答案之前就在思考和尝试一些简单的测试用例,看来它确实在主线程执行。我刚刚创建了一个简单的future{"test"}模板foreach(s => println(Thread.currentThread.getName()),然后在其上打印了该模板main。我误会了吗?
FX

@FX我只是在Scala控制台中做了两次相同的操作(为了同样的未来),并得到了Thread-15Thread-16。它可能取决于Scala版本。
阿列克谢·罗曼诺夫

我认为Scala控制台会为您键入的每个命令生成线程。我只是试过println(...getName()); f.foreach(s => ...getName())(一行)而得到了两次Thread-20。奇怪的。
FX

是的,看来是这样。至少,由于文档没有说它在主线程中被调用,所以我不这么认为。
阿列克谢·罗曼诺夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.