Scala的演员是否与Go的协同程序相似?

问题描述 投票:69回答:5

如果我想移植一个使用Goroutines的Go库,那么Scala会是一个不错的选择,因为它的inbox / akka框架在本质上与协同程序类似吗?

scala go
5个回答
125
投票

不,他们不是。 Goroutines基于通信顺序过程理论,正如Tony Hoare在1978年所指出的那样。这个想法是可以有两个进程或线程彼此独立地行动但共享一个“通道”,一个进程/线程放置数据进入和其他进程/线程消耗。你会发现最突出的实现是Go的频道和Clojure的core.async,但是目前它们仅限于当前的运行时,即使在同一个物理盒上的两个运行时之间也无法分发。

CSP演变为包含一个静态的,正式的过程代数,用于证明代码中存在死锁。这是一个非常好的功能,但Goroutines和core.async目前都不支持它。如果他们这样做,那么在运行代码之前知道是否存在死锁是非常好的。但是,CSP不能以有意义的方式支持容错,因此您作为开发人员必须弄清楚如何处理可能在通道两侧发生的故障,并且这样的逻辑最终会遍布整个应用程序。

1973年Carl Hewitt指定的演员涉及拥有自己邮箱的实体。它们本质上是异步的,并且具有跨越运行时和机器的位置透明性 - 如果你有一个actor的引用(Akka)或PID(Erlang),你可以给它发消息。这也是一些人在基于Actor的实现中发现错误的地方,因为你必须引用另一个actor才能向它发送消息,从而直接耦合发送者和接收者。在CSP模型中,信道是共享的,并且可以由多个生产者和消费者共享。根据我的经验,这不是一个问题。我喜欢代理引用的想法,这意味着我的代码没有散布着如何发送消息的实现细节 - 我只发送一个,并且无论actor位于何处,它都会收到它。如果该节点发生故障并且演员在其他地方转世,那么理论上它对我来说是透明的。

演员有另一个非常好的功能 - 容错。通过按照Erlang中设计的OTP规范将actor组织到监督层次结构中,您可以在应用程序中构建失败域。就像值类/ DTO /您想要调用它们一样,您可以模拟失败,如何处理它以及层次结构的级别。这非常强大,因为您在CSP中几乎没有故障处理能力。

Actor也是一个并发范例,其中actor可以在其中具有可变状态,并且保证不会对状态进行多线程访问,除非构建基于actor的系统的开发人员意外地引入它,例如通过将Actor注册为侦听器用于回调,或通过Futures在actor中异步。

无耻的插件 - 我正在与Akka团队的负责人Roland Kuhn一起写一本新书,称为Reactive Design Patterns,我们将讨论所有这些以及更多内容。绿色线程,CSP,事件循环,Iteratees,Reactive Extensions,Actors,Futures / Promises等。期待在下个月初看到Manning的MEAP。

祝好运!


51
投票

这里有两个问题:

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

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

当然有很多关于为什么Scala作为一种语言更好或更差的观点(例如here是我的),但这些只是意见,不要让它们阻止你。由于Scala是通用的,它“几乎”归结为:您可以在语言X中执行的所有操作,您可以在Scala中执行。如果它听起来太宽......那么continuations in Java :)

  • Scala演员是否与goroutines相似?

唯一的相似之处(除了挑剔)是它们都与并发和消息传递有关。但这就是相似性结束的地方。

由于Jamie的回答很好地概述了Scala演员,我将更多地关注Goroutines / core.async,但是有一些演员模型介绍。

Actors help things to be "worry free distributed"


“无忧”片段通常与以下术语相关联:fault toleranceresiliencyavailability等。

没有详细说明演员的工作方式,演员可以用两个简单的术语来处理:

  • 位置:每个actor都有一个地址/引用,其他actor可以使用它来发送消息
  • 行为:当消息到达actor时应用/调用的函数

想想“谈话过程”,其中每个进程都有一个引用和一个在消息到达时被调用的函数。

当然还有更多内容(例如,查看Erlang OTPakka docs),但上述两个是一个好的开始。

与演员有趣的地方是......实施。目前,两个大的是Erlang OTP和Scala AKKA。虽然它们都旨在解决同样的问题,但存在一些差异。我们来看几个:

  • 我故意不使用诸如“引用透明度”,“幂等”等术语。除了引起混淆之外它们没有好处,所以让我们谈谈不变性[can't change that概念]。 Erlang作为一种语言是固执己见的,它倾向于强烈的不变性,而在Scala中,很容易让演员在收到消息时改变/改变他们的状态。不推荐,但Scala中的可变性就在你面前,人们确实使用它。
  • Joe Armstrong谈到的另一个有趣的观点是,Scala / AKKA受到JVM的限制,而JVM实际上并没有真正考虑到“被分发”,而Erlang VM则是。它涉及许多事情,例如:进程隔离,每个进程与整个VM垃圾收集,类加载,进程调度等。

上述观点并不是说一个比另一个更好,而是表明作为一个概念的演员模型的纯度取决于它的实现。

现在来到goroutines ..

Goroutines help to reason about concurrency sequentially


正如其他已经提到的答案,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个线程在阻塞方法中完成,同时花费相同的时间。

上面描述的踢球者是:“线程被停放”,当他们没有工作时,这意味着,他们的状态被“卸载”到状态机,而实际的实时JVM线程可以自由地做其他工作(source用于一个伟大的视觉)

注意:在core.async中,channel可以在“go block”之外使用,它将由没有停放能力的JVM线程支持:例如如果它阻塞,它会阻止真正的线程。

Go频道的力量

“Goroutines”/“go blocks”中的另一个重要的事情是可以在频道上执行的操作。例如,可以创建一个timeout channel,它将在X毫秒内关闭。或者选择/ 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可能不是您的第一语言,但您不能不同意这种并发实现的顺序和感觉。

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

去UI去吧!

由于core.async“go blocks”具有“停放”执行状态的能力,并且在处理并发时具有非常顺序的“外观和感觉”,JavaScript如何? JavaScript中没有并发性,因为只有一个线程,对吧?并发模仿的方式是通过1024次回调。

但它不一定是这样的。来自wracer的上述示例实际上是用ClojureScript编写的,它编译为JavaScript。是的,它可以在具有许多线程和/或浏览器的服务器上运行:代码可以保持不变。

Goroutines与core.async

同样,一些实现差异[还有更多]强调理论概念在实践中并非一对一的事实:

  • 在Go中,键入了一个频道,在core.async中它不是:例如在core.async中,您可以将任何类型的消息放在同一个通道上。
  • 在Go中,您可以在频道上添加可变内容。不推荐,但你可以。在core.async中,通过Clojure设计,所有数据结构都是不可变的,因此通道内的数据对于它的健康感觉更安全。

So what's the verdict?


我希望上面阐述了演员模型和CSP之间的差异。

不要引起火焰战争,而是给你另一个视角让我们说Rich Hickey:

“我仍然对演员不感兴趣。他们仍然将制作人与消费者联系起来。是的,人们可以模仿或实现某些类型的队列与演员(尤其是人们经常这样做),但由于任何演员机制已经包含了队列,似乎很明显,队列更原始。应该注意的是,Clojure的并发使用状态的机制仍然可行,并且通道面向系统的流程方面。“(source

然而,在实践中,Whatsapp基于Erlang OTP,它似乎卖得很好。

另一个有趣的引用来自Rob Pike:

“缓冲发送未向发送方确认,并且可以任意长。缓冲的通道和goroutine非常接近于actor模型。

演员模型和Go之间的真正区别在于渠道是一等公民。同样重要的是:它们是间接的,如文件描述符而不是文件名,允许在actor模型中不易表达的并发风格。还有一些情况正好相反;我没有做出价值判断。从理论上讲,模型是等价的。“(source


8
投票

将我的一些评论转移到答案。这太长了:D(不要带走jamie和tolitius的帖子;它们都是非常有用的答案。)

你可以做与Akka中goroutine完全相同的事情并不完全正确。 Go通道通常用作同步点。您不能直接在Akka中重现。在Akka中,后同步处理必须移动到一个单独的处理程序中(在jamie的话中用“散布”字样:D)。我会说设计模式不同。你可以用chan启动goroutine,做一些东西,然后<-等待它完成然后继续前进。 Akka与ask有一种不太强大的形式,但ask并不是真正的Akka方式IMO。

Chans也是键入的,而邮箱则不是。这对IMO来说是一个大问题,对于基于Scala的系统来说这是相当令人震惊的。我知道become很难用类型消息来实现,但也许这表明become不是很像Scala。关于阿卡,我可以这么说。它经常感觉就像在Scala上运行一样。 Goroutines是Go存在的关键原因。

别误会我的意思;我非常喜欢演员模特,我一般都喜欢Akka并且觉得工作很愉快。我也一般喜欢Go(我发现Scala很漂亮,而我发现Go只是有用;但它非常有用)。

但容错实际上是Akka IMO的重点。你碰巧得到了并发性。并发是goroutines的核心。容错是Go中的一个单独的东西,委托给deferrecover,它可以用来实现相当多的容错。 Akka的容错更正式,功能更丰富,但也可能更复杂一些。

所有人都表示,尽管有一些相似之处,Akka并不是Go的超集,但它们在功能上有很大差异。 Akka和Go在如何鼓励你处理问题方面有很大的不同,而且在一方面容易的事情是尴尬的,不切实际的,或者至少在另一方面是非惯用的。这是任何系统中的关键区别。

所以把它带回到你的实际问题:我强烈建议在将它带到Scala或Akka之前重新考虑Go接口(这也是IMO的不同之处)。确保按照目标环境的方式进行操作。复杂的Go库的直接端口可能不适合任何一种环境。


6
投票

这些都是伟大而彻底的答案。但是以一种简单的方式来看待它,这是我的观点。 Goroutines是Actors的简单抽象。演员只是Goroutines的一个更具体的用例。

您可以通过创建除了Channel之外的Goroutine来使用Goroutines实现Actors。通过决定该通道被Goroutine“拥有”,你说只有那个Goroutine会从中消耗掉它。你的Goroutine只是在该频道上运行一个收件箱消息匹配循环。然后,您可以简单地将频道作为“演员”(Goroutine)的“地址”传递。

但是由于Goroutines是一种抽象,比演员更通用的设计,Goroutines可以用于比Actors更多的任务和设计。

然而,权衡是因为Actors是一个更具体的案例,像Erlang这样的演员的实现可以更好地优化它们(在收件箱循环中的轨道递归)并且可以更容易地提供其他内置功能(多进程和机器角色) 。


1
投票

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

在Go频道中,您向频道发送消息,并且任何数量的收件人都可以收听,其中一个将收到该消息。

在Actor中只有一个actor,你的actor-ref发送消息,将收到消息。

© www.soinside.com 2019 - 2024. All rights reserved.