Scala 真的是异步的吗

Is Scala Truly Asynchronous

最近我正在研究 Scala 以编写非阻塞 Restful API。我已经在 node.js 中完成了几个项目,这次很想研究一下 Scala。

到目前为止,我的发现是不可能在 Scala 中编写 100% 的异步代码,因为大多数底层代码都是阻塞的。您可以找到有关如何在 Scala 应用程序中使用 JDBC 进行数据库连接的教程。 JDBC 阻塞意味着任何使用它的应用程序都无法在单线程环境中工作,因为它会在等待 DB 时排队所有其他请求 response.Play 框架建议在使用阻塞时增加线程池 APIs 这意味着我们要回到我们开始的地方。

至于node.js,几乎所有可用的模块都是异步编写的。特别是所有数据库连接器和文件处理程序 read/write 异步记住,当你有一个线程时,没有任何东西可以阻塞。然而,我看到人们更喜欢 Scala 而不是 node.js,因为它在异步时具有类型安全性和计算能力。但是异步行为并没有反映在大多数在线可用的教程和资源中。

我的问题是,只有我不明白这一点,还是在 JVM 世界中混淆了异步的真正含义?

更新:

我知道 JDBC 同步不会使 Scala 同步,但我看到有人争辩说他们的 API 是非阻塞的,但他们正在使用同步库。此外,如果大多数驱动程序和库仍然阻塞,如何在 Scala 中编写非阻塞应用程序。即使有替代方案,这也意味着您在使用库检查其内部结构以查看其阻塞或非阻塞时必须非常小心,node.js.

并非如此。

Scala 建立在 JVM 之上,JVM 世界中的大部分代码,包括 JDBC 都是为 Java 编写的。 Scala 试图鼓励您使用 Futures 或 Akka Actors 编写非阻塞代码,但显然就像在任何编程语言中一样,您可以做您想做的事。

所以对于你的问题,scala 不是 100% 非阻塞的,我的意思是如果你愿意,你甚至可以使用来自 Java 的同步块。但是我们鼓励并思考阻塞代码的缺点以及如何避免它(不可变、无状态等),并且支持您通过语言中的特性编写非阻塞代码(用于转换数据而不是更改数据的单子)一种可变的方式)。

用 Scala(和 Java)编写异步应用程序是可能的(我什至会说很容易)但是你必须小心第三方库,知道哪些 API:s 是阻塞的,哪些是异步的。

例如,Java 标准库提供了阻塞和非阻塞 IO API:s。

JDBC 是另一个常见的 API 示例,它本质上是阻塞的,如果你想编写 100% 异步应用程序,你只需要为你的数据库使用不同的客户端。

与 node.js 的一大区别是您实际上可以处理阻塞 API:s 和异步内容。这是可能的,因为我们在 JVM 中有多线程,并且可以 运行 在为该任务调整的单独线程池中阻塞的东西。因此,如果我们有 8 个可能阻塞的数据库连接,我们可以 运行 那些在具有 8 个线程的线程池中,而不会阻塞 "regular" 线程池中的任何异步代码。

附带说明一下,我们拥有并发性还允许我们每个内核有一个线程来并行执行 运行 异步代码,而不是整个 vm 一个线程。

用 Scala 编写完全异步的代码绝对是可能的。有很多响应式库,TypeSafe 付出了很多努力来提供响应式 platform/ecosystem。为了说服自己,看看 awesome scala and search "Reactive", or follow TypeSafe webinars.

我不知道Node.js,但我认为你可以写这样一个阻塞函数:

function aBlockingFunction() {
  while(resourceNotAvailable) {
  }
  ...
}

Scala 不会神奇地使同步代码异步,但它会为您提供编写完全异步代码所需的一切(Futures/Promises, Actors、Streams,...)。

异步并不意味着单线程,它意味着您在等待回复的同时不断进行有用的思考。 Play 被设计为完全异步的,但您仍然可以编写阻塞代码(例如调用同步 API)。但是,如果您编写完全异步的,是的,您可以 运行 单线程。为此,只需使用 Futures/Actors/...

Scala 并发是这样建模的:有一个(实际上有很多)线程池对应于并行执行,并且有任务要做(Futures/Actors/...)。任务在可用线程上分派。当一个任务需要等待回复或让出时,线程被分配给另一个准备就绪的任务,依此类推。你的计算看起来是 "single threaded" 实际上是由许多小的协调迷你任务组成的 运行 在任何可用的线程上(通常不相同)。

默认情况下,Play thread pools have as many threads as available processors。这是完美的,因为你的代码是异步的,因为线程永远不会空闲(当一个任务空闲时,另一个任务将取代它)。但是当执行阻塞操作时,你会阻塞胎面,因此在操作完成之前你将少一个处理器。

这是众所周知的 co-operative/pre-emptive 问题。 Co-operative 更轻更快,因为它不需要保存那么多上下文(它可以像函数调用一样轻)但是一个任务可以阻塞其他任务。

这就是为什么,当使用大量阻塞调用时,Play 建议增加线程池(实际上你应该创建另一个线程池专用于阻塞操作)。理想情况下,您应该分配 "number_of_simultaneous_blocking_operations + number_of_possible_parallel_executions" 个线程,以便始终有一个线程可用于就绪任务。

我的建议是:选择异步机制 (Futures/Actors/Streams/...) 和响应式库。