首页 > 解决方案 > Scala Futures 如何在线程上运行?以及如何使用它们来执行异步和非阻塞代码?

问题描述

据我了解,在 Scala 中执行 IO 有 3 种方式,我将尝试用伪代码来表达。

、同步&阻塞:

val syncAndBlocking: HttpResponse = someHttpClient.get("foo.com")

这里主线程只是空闲,直到响应返回..

其次,异步但仍然阻塞:

val asyncButBlocking: Future[HttpResponse] = Future { someHttpClient.get("bar.com") }

据我了解,这里的主线程是空闲的(因为 Future 在单独的线程上执行)但是该单独的线程被阻塞了..

第三,异步和非阻塞。我不确定如何实现那个,但据我所知,实现(例如 http 客户端)本身必须是非阻塞的,所以类似于:

val asynAndNotBlocking: Future[HttpResponse] = Future { someNonBlockingHttpClient.get("baz.com") }

我的问题是:

  1. 我上述假设是否有效?
  2. Scala 期货是在 OS 线程还是绿色线程上运行?或者这取决于执行上下文?
  3. 在 IO 是异步和非阻塞的第三种情况下,它是如何工作的?线程是否只启动任务(例如发送获取请求),然后再次空闲,直到响应到达时通过某种事件循环通知它?

受以下参考资料启发的问题:here & here

标签: multithreadingscala

解决方案


val syncAndBlocking: HttpResponse = someHttpClient.get("foo.com")

这将阻塞调用线程,直到收到响应(如您所述)。

val asyncButBlocking: Future[HttpResponse] = Future { someHttpClient.get("bar.com") }

与对 的任何调用一样Future.apply,这(至少在概念上,可能存在消除某些步骤的优化):

  1. 创建一个Function0[HttpResponse]方法applysomeHttpClient.get("bar.com"). 如果someHttpClient是静态的,理论上这可能发生在编译时(我不知道 Scala 编译器是否会执行此优化),否则它将发生在运行时。
  2. 将 thunk 传递给Future.apply,然后:
  3. 创建一个Promise[HttpResponse].
  4. ExecutionContext在隐式传递的 to 上安排任务Future.apply。此任务是调用 thunk:如果 thunk 成功执行,则与 thunk的结果Promise相关联Future(成功)完成,否则Future失败(也是完成)与抛出的Throwable(它可能仅在Throwable匹配时失败) by NonFatal,我懒得检查,在这种情况下致命的抛出被ExecutionContext) 捕获。
  5. 一旦在 thunk 上安排了任务ExecutionContext(可能在也可能不在 thunk 执行之前),就会返回Future与 相关的。Promise

thunk 如何执行的细节取决于ExecutionContextScala 运行时的细节和扩展(对于 JVM 上的 Scala,thunk 将在由 确定的线程上运行ExecutionContext,无论这是 OS 线程还是绿色线程依赖于 JVM,但操作系统线程至少目前可能是一个安全的假设(Project Loom 可能会影响这一点);对于 ScalaJS(因为 JavaScript 不公开线程)和 Scala Native(据我目前所知:可以想象可以ExecutionContext使用操作系统线程,但在运行时会存在诸如 GC 之类的风险),这可能是一个带有全局线程的事件循环)。

调用线程在第 5 步执行之前被阻塞,所以从调用者的角度来看,它是非阻塞的,但是某处有一个线程被阻塞。

val asynAndNotBlocking: Future[HttpResponse] = Future { someNonBlockingHttpClient.get("baz.com") }

...可能不会进行类型检查(假设它与HttpResponse上面的类型相同),因为为了非阻塞,HttpResponse必须将其包装在表示异步/非阻塞的类型中(例如Future),所以asyncAndNotBlocking是type Future[Future[HttpResponse]],在一些特定用例之外,这是一种毫无意义的类型。你更有可能拥有类似的东西:

val asyncAndNotBlocking: Future[HttpResponse] = someNonBlockingHttpClient.get("baz.com")

或者,如果someNonBlockingHttpClient不是 Scala 原生并返回一些其他异步库,您将拥有

val asyncAndNotBlocking: Future[HttpResponse] = SomeConversionToFuture(someNonBlockingHttpClient.get("baz.com"))

SomeConversionToFuture基本上就像上面的草图Future.apply,但是可以,而不是使用ExecutionContext其他异步库中的使用操作来运行代码以在完成Future.get完成关联。

如果您Future[Future[HttpResponse]]出于某种原因真的想要,考虑到它可能someNonBlockingHttpClient会很快返回.get(请记住,它是异步的,因此它可以早在请求被设置并安排发送时返回),Future.apply可能不是要走的路关于事情:步骤1-5的开销可能比花费的时间更长.get!对于这种情况,Future.successful很有用:

val doubleWrapped: Future[Future[HttpResponse]] = Future.successful( someNonBlockingHttpClient.get("baz.com"))

Future.successful不涉及 thunk、创建Promise或安排任务ExecutionContext(它甚至不使用ExecutionContext!)。它直接构造一个已经成功完成的Future,但其中包含的值Future是在调用之前计算的(即执行 thunk 中的内容)Future.successful并阻塞调用线程。如果有问题的代码阻塞了足够长的时间来设置异步执行的东西,这不是问题,但是它可以使长时间阻塞的东西看起来像异步/非阻塞一样。

知道什么时候使用Future.applyFuture.successful是很重要的,特别是如果你关心性能和可伸缩性。看到Future.successful使用不当可能比Future.apply(因为它不需要隐含的ExecutionContext,我已经看到新手被它所吸引)更常见。正如 Colin Breck 所说,不要通过不当使用Future.successful.


推荐阅读