这里有两个问题:
- Scala 是移植
goroutines 的好选择吗?
这是一个简单的问题,因为 Scala 是一种通用语言,它并不比您可以选择“移植 goroutines”的许多其他语言更差或更好。
当然有很多意见关于为什么 Scala 更好或更差的原因作为一种语言(例如 here 是我的),但这些只是意见,不要不要让他们阻止你。
由于 Scala 是通用的,它“几乎”归结为:你可以用 X 语言做的所有事情,你都可以用 Scala 做。如果听起来太宽泛.. continuations in Java 怎么样 :)
- Scala actor 是否类似于
goroutines?
唯一的相似之处(除了吹毛求疵)是它们都与并发和消息传递有关。但这就是相似之处的结束。
由于 Jamie 的回答很好地概述了 Scala 演员,我将更多地关注 Goroutines/core.async,但会介绍一些演员模型。
演员帮事情“无忧分发”
“无忧”文章通常与以下术语相关联:fault tolerance、resiliency、availability 等。
在不详细说明演员如何工作的情况下,简而言之,演员必须处理以下两个方面:
-
位置:每个参与者都有一个地址/引用,其他参与者可以使用该地址/引用向其发送消息
-
行为:当消息到达参与者时应用/调用的函数
想想“谈话进程”,其中每个进程都有一个引用和一个在消息到达时被调用的函数。
当然还有更多内容(例如查看Erlang OTP 或akka docs),但以上两个是一个好的开始。
演员的有趣之处在于......实施。目前,两个大的是 Erlang OTP 和 Scala AKKA。尽管它们都旨在解决同一问题,但存在一些差异。让我们看一对:
-
我故意不使用诸如“引用透明性”、“幂等性”等术语。它们除了引起混乱之外没有任何好处,所以我们只讨论不变性[can't change that 概念]。 Erlang 作为一门语言是固执己见的,它倾向于强大的不变性,而在 Scala 中,很容易让参与者在收到消息时改变/改变他们的状态。不建议这样做,但 Scala 中的可变性就在您面前,人们确实使用它。
-
Joe Armstrong 谈到的另一个有趣的一点是,Scala/AKKA 受到 JVM 的限制,而 JVM 的设计并没有真正考虑到“分布式”,而 Erlang VM 是。它与许多事情有关,例如:进程隔离、每个进程与整个 VM 垃圾收集、类加载、进程调度等。
上面的意思并不是说一个比另一个更好,而是表明actor模型作为一个概念的纯度取决于它的实现。
现在到 goroutines..
Goroutines 有助于顺序推理并发
正如已经提到的其他答案,goroutines 起源于 Communicating Sequential Processes,这是一种“用于描述并发系统中交互模式的正式语言”,根据定义,它几乎可以表示任何含义:)
我将根据core.async 给出示例,因为我比 Goroutines 更了解它的内部结构。但是core.async是在Goroutines/CSP模型之后构建的,所以在概念上应该不会有太多的区别。
core.async/Goroutine 中的主要并发原语是channel。将channel 视为“岩石上的队列”。该通道用于“传递”消息。任何想要“参与游戏”的进程都会创建或获取对 channel 的引用,并向其发送/接收(例如发送/接收)消息。
24 小时免费停车
在通道上完成的大部分工作通常发生在“Goroutine”或“go block”中,“获取它的主体并检查它是否有任何通道操作。它将把主体变成一个状态机。在达到任何阻塞操作时,状态机将被“停放”并释放实际的控制线程。这种方法类似于 C# async 中使用的方法。当阻塞操作完成时,代码将恢复(在线程池线程或 JS VM 中的唯一线程上)" (source)。
用视觉传达要容易得多。以下是阻塞 IO 执行的样子:
您可以看到线程大部分时间都在等待工作。这是相同的工作,但通过“Goroutine”/“go block”方法完成:
这里 2 个线程完成了所有工作,而 4 个线程以阻塞方式完成了所有工作,同时花费了相同的时间。
上面描述的关键是:“线程parked”当它们没有工作时,这意味着它们的状态被“卸载”到状态机,并且实际的活动JVM线程是空闲的做其他工作(source 以获得出色的视觉效果)
注意:在 core.async 中,通道可以在“go block”之外使用,这将由没有停放能力的 JVM 线程支持:例如如果它阻塞,它会阻塞真正的线程。
Go 频道的力量
“Goroutines”/“go blocks”中的另一个重要内容是可以在通道上执行的操作。例如,可以创建一个timeout channel,它将在 X 毫秒内关闭。或者 select/alt! 函数,当与许多通道一起使用时,就像跨不同通道的“你准备好了吗”轮询机制一样工作。将其视为非阻塞 IO 中的套接字选择器。以下是一起使用timeout channel 和alt! 的示例:
(defn race [q]
(searching [:.yahoo :.google :.bing])
(let [t (timeout timeout-ms)
start (now)]
(go
(alt!
(GET (str "/yahoo?q=" q)) ([v] (winner :.yahoo v (took start)))
(GET (str "/bing?q=" q)) ([v] (winner :.bing v (took start)))
(GET (str "/google?q=" q)) ([v] (winner :.google v (took start)))
t ([v] (show-timeout timeout-ms))))))
这段代码 sn-p 取自 wracer,它向所有三个:Yahoo、Bing 和 Google 发送相同的请求,并从最快的一个返回结果,或超时(返回超时消息)如果在给定时间内没有返回。 Clojure 可能不是您的第一语言,但您不能不同意这种并发实现的外观和感觉如何顺序。
您还可以合并/扇入/扇出来自/到多个通道的数据,映射/减少/过滤/...通道数据等等。频道也是一等公民:您可以将频道传递给频道..
去 UI 去!
既然 core.async "go blocks" 有这种能力来 "park" 执行状态,并且在处理并发时有一个非常顺序的 "look and feel",那么 JavaScript 呢? JavaScript 中没有并发,因为只有一个线程,对吧?并且模仿并发的方式是通过 1024 回调。
但不一定非要这样。上面来自wracer 的示例实际上是用ClojureScript 编写的,可以编译成JavaScript。是的,它可以在具有多个线程的服务器上和/或在浏览器中工作:代码可以保持不变。
Goroutines 与 core.async
再次,一些实现差异 [还有更多] 强调了理论概念在实践中并不完全是一对一的事实:
- 在 Go 中,输入了通道,而在 core.async 中则没有:例如在 core.async 中,您可以将任何类型的消息放在同一个频道上。
- 在 Go 中,您可以将可变的东西放在通道上。不推荐,但可以。在 core.async 中,由 Clojure 设计,所有数据结构都是不可变的,因此通道内的数据对其健康感觉更安全。
那么判决结果是什么?
我希望以上内容能够阐明演员模型和 CSP 之间的差异。
不是为了引发一场激烈的战争,而是为了给你另一个视角,比如说 Rich Hickey:
“我仍然对演员不感兴趣。他们仍然将生产者与消费者结合在一起。是的,可以模拟或实现某些类型的演员队列(尤其是,人们经常这样做),但由于任何演员机制已经包含队列,显然队列更原始。应该注意的是,Clojure 的并发使用状态的机制仍然可行,并且通道面向系统的流方面。"(source )
然而,实际上,Whatsapp 是基于 Erlang OTP 的,而且看起来卖得很好。
另一个有趣的引述来自 Rob Pike:
"缓冲的发送没有得到发送者的确认,可能需要任意长的时间。缓冲的通道和 goroutine 非常接近于 Actor 模型。
actor 模型和 Go 的真正区别在于通道是一等公民。同样重要的是:它们是间接的,就像文件描述符而不是文件名一样,允许在参与者模型中不容易表达的并发样式。也有相反的情况。我不是在做价值判断。理论上模型是等价的。"(source)