Scala 的 actors 和 Go 的 coroutines 相似吗?

81

如果我想要移植一个使用Goroutines的Go库,那么Scala是否是一个好选择,因为它的inbox/akka框架在性质上类似于coroutines?


3
Clojure的core.async库可能比akka更适合于go routines。除此之外,您的回答在很大程度上取决于开发人员愿意使用/学习什么。 - dhable
@Dan,这并不是主观的,我正在寻找一种1:1的功能比较方法,以便于移植而不需要重新编写库,因为语言差异非常大。但你可能有一定道理... - loyalflow
你可以查看 https://github.com/rssh/scala-gopher 来了解在 Scala 中类似 Go 的 CSP 原语。 - rssh
5个回答

161
不是的,Goroutines基于Tony Hoare在1978年规定的通信顺序进程理论。这个想法是存在两个进程或线程可以独立地行动,但共享一个“通道”,其中一个进程/线程将数据放入该通道中,而另一个进程/线程则消耗它。你会发现最突出的实现是Go语言中的通道和Clojure的core.async,但目前它们仅限于当前运行时,并且不可以在同一物理盒上的两个运行时之间进行分布式处理。
CSP演变成包括用于证明代码中死锁存在的静态、形式化进程代数。这是一个非常好的功能,但Goroutines和core.async都不支持它。如果有一天他们支持了,那么在运行您的代码之前知道死锁是否可能发生将是极好的。然而,CSP不能以有意义的方式支持容错性,因此作为开发人员,你必须想办法处理可能发生在通道两侧的故障,并且这样的逻辑最终会被散布在整个应用程序中。
Actor由Carl Hewitt在1973年规定,涉及具有自己邮箱的实体。它们本质上是异步的,并且具有跨运行时和机器的位置透明性——如果您有一个Actor的引用(Akka)或PID(Erlang),那么您可以向其发送消息。这也是一些人在基于Actor的实现中发现缺点的地方,在那里,您必须拥有对另一个Actor的引用才能向其发送消息,从而直接耦合了发送方和接收方。在CSP模型中,通道是共享的,并且可以被多个生产者和消费者共享。根据我的经验,这并不是很重要。我喜欢代理引用的想法,这意味着我的代码没有被实现细节所淹没,如何发送消息-我只需要发送一条消息,无论Actor位于何处,它都会收到。如果该节点关闭并重新启动Actor,则理论上对我来说是透明的。

演员还有另一个非常不错的特性 - 容错性。按照Erlang中OTP规范将演员组织成监督层次结构,您可以在应用程序中构建一定程度的故障域。就像价值类/ DTOs /任何您想称呼它们的东西一样,您可以对失败进行建模,了解应该如何处理以及在层次结构的哪个级别上处理。这非常强大,因为CSP内部几乎没有故障处理能力。

演员也是并发编程范式,其中演员可以在其内部具有可变状态,并保证无多线程访问该状态,除非基于演员的系统开发人员意外地引入它,例如通过注册Actor作为回调的侦听器或通过Futures在演员内部进行异步操作。

厚颜无耻地打广告 - 我正在与Akka团队负责人Roland Kuhn写一本新书,名为Reactive Design Patterns,其中我们讨论了所有这些以及更多内容。Green threads、CSP、事件循环、Iteratees、Reactive扩展、Actors、Futures/Promises等。预计下个月初在Manning上看到MEAP。


7
+0.75 :) 我认为“不”太强烈了。在消息传递的意义上有相似之处。细节和功能是不同的,但最终目标非常相似。两者都试图通过消息传递来解决并发编程的问题。 - nicerobot
5
这不是不合理的。尽管通道生产者与通道消费者相遇的连接点在时间上有点耦合,但它们都是异步的。它们都基于消息。在我看来,演员模型非常适合容错和跨节点扩展,而CSP是利用节点内多个线程的有效机制。 - jamie
3
@user1361315,并不完全正确地说,您可以使用Akka做完全相同的事情。Go通道通常用作同步点。您无法直接在Akka中复制它。在Akka中,后同步处理必须移动到单独的处理程序中(按Jamie的话说是"strewn" :D)。我会说设计模式是不同的。您可以使用chan启动goroutine,做一些工作,然后<-等待完成后再继续。Akka有一种不太强大的形式,称为ask,但在我看来,ask并不是真正的Akka方式。通道也具有类型,而邮箱则没有。 - Rob Napier
1
@jamie,Akka是否真正具有反应性:http://programmers.stackexchange.com/q/255047/13154? - Den
1
读到这里时,我想:“嗯,这很易读,同时展现了对理论和实践的深刻理解。”然后我注意到署名——“啊,这就解释了一切”。 - Matthew Mark Miller
显示剩余2条评论

63

这里有两个问题:

  • Scala是将goroutines移植的好选择吗?

这是一个简单的问题,因为Scala是一种通用语言,与许多其他你可以选择的语言一样好或不好来“移植goroutines”。

当然,对于Scala作为一种语言更好或更差的观点有很多(例如here是我的),但这些只是观点,请不要让它们阻止你。 由于Scala是通用的,它“几乎”归结为:在语言X中你能做的所有事情,你都可以在Scala中做。如果听起来太广泛了...那么continuations in Java怎么样?

  • Scala的actors和goroutines相似吗?

唯一的相似之处(除了小毛病)是它们都涉及并发和消息传递。但这就是相似之处的结束。

由于Jamie的回答已经很好地概述了Scala actors,因此我将更加专注于Goroutines/core.async,但是会简单介绍一下actor模型。

Actors帮助实现“无忧分布式”


“无忧”的代码通常与以下术语相关联:容错性弹性可用性等。

不深入讲解演员如何工作,简单来说,演员涉及以下两个方面:

  • 局部性:每个演员都有一个地址/引用,其他演员可以使用它发送消息
  • 行为:当消息到达演员时要应用/调用的函数

可以将其视为“对话进程”,其中每个进程都具有引用和在消息到达时调用的函数。

当然还有更多内容(例如,请查看Erlang OTPakka docs),但以上两点是一个良好的起点。

演员变得有趣的地方在于实现。目前有两个重要的实现,即Erlang OTP和Scala AKKA。虽然它们都旨在解决同样的问题,但存在一些差异。让我们看看其中的一些:

  • 我有意不使用诸如“引用透明”、“幂等性”等术语,这些术语除了造成混淆没有任何好处,所以让我们只谈论不可变性[一个“不能改变”的概念]。作为一种语言,Erlang是有偏见的,并且它倾向于强烈的不可变性,而在Scala中,当接收到消息时,很容易制作会更改/突变其状态的Actor。虽然不建议这样做,但是Scala中的可变性就在你面前,人们确实使用它。

  • Joe Armstrong谈到的另一个有趣的观点是,Scala/AKKA受到JVM的限制,而JVM并没有真正考虑“分布式”,而Erlang VM则考虑到了。这与许多因素有关,例如:进程隔离、每个进程与整个VM垃圾收集、类加载、进程调度等等。

以上内容的重点不是说哪一个比另一个更好,而是要表明Actor模型纯度作为一个概念取决于其实现方式。

现在来谈谈Goroutines..

Goroutines帮助按顺序推理并发


正如其他答案已经提到的那样,goroutine 源于通信顺序进程,这是一种“描述并发系统中交互模式的形式语言”,根据定义可以意味着几乎任何东西 :)
我将基于 core.async 给出例子,因为我比 Goroutines 更了解它的内部。但是 core.async 是在 Goroutines/CSP 模型之后构建的,因此概念上应该没有太多区别。
core.async/Goroutine 中的主要并发原语是channel。将 channel 视为“岩石上的队列”。使用此通道来“传递”消息。任何想要“参与游戏”的过程都会创建或获取对 channel 的引用,并将消息放入/从中取出(例如发送/接收)。
大多数通道操作都在“Goroutine”或“go block”中执行,其会检查其主体中是否有任何通道操作。它将主体转换为状态机。在达到任何阻塞操作时,状态机将被“停放”,实际的控制线程将被释放。这种方法类似于C# async。当阻塞操作完成时,代码将恢复执行(在线程池线程或JS VM中的唯一线程上)(source)。使用可视化工具更容易理解,以下是阻塞IO执行的示意图:

blocking IO

你可以看到线程大多数时间都在等待工作。这里是相同工作,但使用“Goroutine”/“go块”方法完成:

core.async

这里只用了2个线程完成了4个线程在阻塞模式下的所有工作,同时花费相同的时间。

上述描述中的关键是:"当线程没有工作时,它们会被停放",这意味着它们的状态被"卸载"到一个状态机中,实际的JVM线程可以自由地进行其他工作(source提供了很好的可视化效果)

注意:在core.async中,通道可以在"go block"之外使用,这将由一个没有停车能力的JVM线程支持:例如,如果它被阻塞,它将阻塞真正的线程。

Go通道的威力

在“Goroutines” /“go blocks”中的另一个重要事项是可以对通道执行的操作。例如,可以创建一个timeout channel,它将在X毫秒后关闭。或者使用select/alt!函数,与许多通道一起使用时,它就像跨不同通道的“准备好了吗”轮询机制。将其视为非阻塞IO中的套接字选择器。以下是同时使用timeout channelalt!的示例:

(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))))))

这段代码摘自wracer,它向Yahoo、Bing和Google发送相同的请求,并从最快的一个返回结果,或者如果在给定时间内没有返回,则超时(返回超时消息)。Clojure可能不是您的第一语言,但您不能否认这种并发实现的顺序性和感觉。

您还可以从/向许多通道合并/扇入/扇出数据,映射/减少/过滤/...通道数据等。通道也是一等公民:您可以将通道传递给通道。

Go UI Go!

由于核心.async "go块"具有“停放”执行状态的能力,并且在处理并发时具有非常顺序的“外观和感觉”,那么JavaScript呢?JavaScript中没有并发,因为只有一个线程,对吗?并发的模拟方式是通过1024个回调函数。

但事实并非如此。上述示例来自wracer,实际上是用ClojureScript编写的,可以编译成JavaScript。 是的,它将在具有许多线程和/或浏览器的服务器上工作:代码可以保持不变。

Goroutines vs. core.async

同样,有几个实现差异[还有更多]强调理论概念在实践中并不完全是一对一的:

  • 在Go中,通道是有类型的,在core.async中则没有:例如,在core.async中,您可以将任何类型的消息放置在同一个通道上。
  • 在Go中,您可以将可变对象放入通道中。虽然不建议这样做,但您可以这样做。在core.async中,按照Clojure的设计,所有数据结构都是不可变的,因此通道内部的数据对其健康状态感到更加安全。

那么什么是裁决?


我希望上述内容能够阐明演员模型和CSP之间的差异。
为了给你提供另一种视角,而不是引发争论,让我们看看Rich Hickey的观点:
“我对演员仍然不感兴趣。它们仍然将生产者与消费者耦合在一起。是的,人们可以用演员来模拟或实现某些类型的队列(值得注意的是,人们经常这样做),但由于任何演员机制已经包含了一个队列,因此似乎很明显队列更为原始。应该注意的是,Clojure的并发状态使用机制仍然可行,并且通道面向系统的流程方面。”(source)
然而,在实践中,WhatsApp基于Erlang OTP,并且似乎卖得很好。
另一个有趣的引用来自Rob Pike:
“缓冲发送不会向发送方确认,并且可能需要任意长的时间。缓冲通道和goroutine非常接近演员模型。”

Actor模型和Go之间真正的区别在于,通道是一等公民。同样重要的是:它们是间接的,就像文件描述符而不是文件名,这使得并发的风格不容易在Actor模型中表达出来。也有一些情况恰恰相反;我不做价值判断。理论上,这些模型是等效的。"(来源)


2
哦,Anatoly。你至少应该把我的名字拼对。 :) - jamie
4
关于 Rich Hickey 的引言,我认为队列的原始性并不是对演员模型不热衷的好理由。指针比引用更原始,然而我们没有为 JVM 设计指针。 - lcn
2
嗨。我是一名已经从事Scala编程5年的程序员。我使用Akka也有大约同样长的时间了。我使用actor的次数越多,我就越不热衷于它们。当过度使用时,它们很容易创建一个非常复杂、难以理解的系统。有些事情演员做得非常好。在我看来,大多数情况下,演员被用在简单的事件循环本来可以同样好或者更好的地方。 - Tim Harper

8
将我的一些评论移到了回答中。它变得太长了:D(不是为了抢夺Jamie和Tolitius的帖子;它们都是非常有用的答案)。
并不完全正确,你可以在Akka中做与goroutines完全相同的事情。Go通道经常用作同步点。你无法直接在Akka中复制它。在Akka中,后同步处理必须移动到单独的处理程序中(用Jamie的话说就是“strewn” :D)。我会说设计模式是不同的。你可以使用 chan 启动goroutine,执行一些操作,然后<-等待它完成后再继续进行。 Akka具有较弱的形式,其中包括 ask ,但是 ask 不是真正的Akka方式,在我看来。
Chans也是有类型的,而邮箱则没有。这对我来说很重要,对于基于Scala的系统来说,这相当令人震惊。我理解 become 很难使用类型化消息实现,但也许这表明 become 与Scala不太像。我可以这样说Akka通常是这样的。它经常感觉像是自己的东西,碰巧在Scala上运行。Goroutines是Go存在的关键原因。
别误会我的意思;我非常喜欢演员模型,并且通常喜欢Akka并发,发现在其中工作很愉快。我也通常喜欢Go(我认为Scala很美丽,而我认为Go只是有用的;但它非常有用)。
但是,在我看来,容错性才是Akka的重点。你碰巧在这方面获得了并发性。并发是goroutines的核心。Go中的容错性是一个单独的事情,委托给 defer recover ,可以用于实现相当多的容错性。 Akka的容错性更加正式和功能丰富,但它也可能会更加复杂。
总之,尽管具有某些相似之处,但Akka不是Go的超集,它们在功能上存在显着分歧。 Akka和Go在鼓励您解决问题的方式方面非常不同,而在其中一个环境中易于处理的事情在另一个环境中则很尴尬,不切实际或至少不符合习惯。这是任何系统的关键区别。
因此,将其带回您实际的问题:在将其带入Scala或Akka之前,我强烈建议重新考虑Go界面(在我看来,它们也是非常不同的东西)。确保您正在以目标环境意味着的方式进行操作。复杂Go库的直接移植可能不适合任何一个环境。

6
这些都是很好且详细的回答。但是,为了简单地看待它,这是我的观点。Goroutines是Actor的简单抽象。Actors只是Goroutines的更具体用例。
您可以通过在Channel旁创建Goroutine来使用Goroutines实现Actors。通过决定该通道由哪个Goroutine“拥有”,您正在表示仅该Goroutine将从中消耗。您的Goroutine只是在该通道上运行一个收件箱-消息匹配循环。然后,您可以将该通道简单地传递为您的“Actor”(Goroutine)的“地址”。
但是,由于Goroutines是一种抽象,比Actor更一般化的设计,因此Goroutines可以用于比Actors更多的任务和设计。
然而,权衡之处在于,由于Actors是更具体的情况,像Erlang这样的actor实现可以更好地优化它们(在收件箱循环上进行铁路递归),并且可以更轻松地提供其他内置功能(多进程和机器actors)。

2

我们可以说,在Actor模型中,可寻址的实体是Actor,即消息的接收者。而在Go通道中,可寻址的实体是通道,即消息流动的管道。

在Go通道中,您将消息发送到通道中,任意数量的接收者都可以监听,并且其中一个将接收消息。

在Actor中,只有一个Actor将接收到您发送的消息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接