Scala的演员类似于Go的协程吗?


76

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


2
Clojure的core.async库可能比akka更适合执行例程。除此之外,您的答案很大程度上取决于开发人员愿意使用/学习的内容。
2014年

@Dan并不是很主观,我正在寻找1:1功能比较,因此移植是不费吹灰之力的,因为反对用语言B / c来重写库,因为语言差异是如此之大。但是您可能有一点...
loyalflow 2014年

您可以在scala中查看github.com/rssh/scala-gopher中的Go-like CSP原语。
rssh 2014年

Answers:


140

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

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

卡尔·休伊特(Carl Hewitt)在1973年指定的演员涉及具有自己邮箱的实体。它们本质上是异步的,并且具有跨运行时和计算机的位置透明性-如果您具有参与者的引用(Akka)或PID(Erlang),则可以向其发送消息。这也是某些人在基于Actor的实现中发现错误的地方,因为您必须引用其他actor才能向其发送消息,从而直接耦合发送方和接收方。在CSP模型中,通道是共享的,并且可以由多个生产者和消费者共享。以我的经验,这并不是什么大问题。我喜欢代理引用的想法,这意味着我的代码中没有乱七八糟的如何发送消息的实现细节-我只发送一条消息,而无论actor位于何处,它都会收到消息。

演员还有另一个很好的功能-容错。通过根据Erlang中制定的OTP规范将参与者组织为监督层次结构,您可以在应用程序中构建故障域。就像值类/ DTO /无论您要调用它们什么一样,您可以对失败,应该如何处理以及在层次结构的哪个级别进行建模。这非常强大,因为CSP内部几乎没有故障处理功能。

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

无耻的插件-我正在与Akka团队的负责人Roland Kuhn一起写一本名为Reactive Design Patterns的新书,我们将讨论所有这些以及更多内容。绿色线程,CSP,事件循环,迭代,响应式扩展,参与者,期货/承诺等。期望在下个月初看到有关Manning的MEAP。

祝好运!


5
+0.75 :)我认为“否”太强了。在消息传递的意义上有相似之处。细节和功能不尽相同,但最终目标却非常相似。两者都试图通过消息传递解决并发编程。
nicerobot 2014年

4
那不是不合理的。它们都是异步的,尽管与频道制作人见面互动的方式相比,频道制作人与频道消费者会面的连接时间更紧密。它们都是基于消息的。在我看来,参与者非常适合容错和跨节点扩展,其中CSP是利用节点内的多个线程的有效机制。
杰米2014年

想法是,每当有东西传递到通道时,我都可以在scala中做同样的事情,但是将其传递给scala actor。我不在乎它在
幕后有

3
@ user1361315,使用Akka可以做完全相同的事情并不是很正确。Go通道通常用作同步点。您不能直接在Akka中复制它。在Akka中,后同步处理必须移到一个单独的处理程序中(用Jamie的话:D表示“ strewn”)。我会说设计模式是不同的。您可以使用chan启动goroutine,进行一些操作,然后<-等待其完成再继续。Akka的形式不那么强大ask,但askIMO并不是Akka的。还会键入Chan,而不会键入邮箱。
罗布·纳皮尔

1
@jamie那么Akka实际上是反应性的 吗:programmers.stackexchange.com/q/255047/13154
2014年

57

这里有两个问题:

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

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

当然,对于为什么Scala作为一种语言更好还是更坏,有很多观点(例如,是我的观点),但这只是观点,不要让它们阻止您。由于Scala是通用的,因此“差不多”可以归结为:您可以使用X语言进行的所有操作,都可以使用Scala进行。如果听起来太宽泛.. Java的延续如何:)

  • Scala演员类似于goroutines吗?

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

由于Jamie的答案很好地概述了Scala演员,因此,我将重点介绍Goroutines / core.async,但会介绍一些演员模型。

演员帮助事情“无忧无虑地分发”


其中“无忧”片通常与诸如有关:fault toleranceresiliencyavailability等。

在不涉及参与者如何工作的严肃细节的情况下,参与者必须用两个简单的术语进行说明:

  • 位置:每个参与者都有一个地址/引用,其他参与者可以使用该地址/引用向其发送消息
  • 行为:当消息到达参与者时被应用/调用的函数

想想“对话过程”,其中每个过程都有一个引用和一个消息到达时调用的功能。

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

与演员一起有趣的地方是..实现。目前,有两个大公司是Erlang OTP和Scala AKKA。他们俩都致力于解决同一件事,但存在一些差异。让我们来看几个:

  • 我故意不使用诸如“参照透明性”,“幂等性”之类的术语。它们除了造成混乱之外没有其他好处,所以我们只讨论不变性[一个can't change that概念]。人们普遍认为Erlang是一种语言,它倾向于强大的不变性,而在Scala中,让参与者在收到消息时更改/更改其状态太容易了。不建议这样做,但是Scala中的可变性就在您的面前,并且人们确实在使用它。

  • Joe Armstrong谈到的另一个有趣的观点是,Scala / AKKA受JVM的限制,而JVM并不是真正被设计为“分散”的,而Erlang VM却是。它与许多事情有关,例如:进程隔离,每个进程与整个VM垃圾收集,类加载,进程调度等。

上面的意思并不是说一个优于另一个,而是要表明参与者模型作为一个概念的纯度取决于它的实现。

现在到goroutines。

Goroutine有助于顺序推理并发


正如其他答案已经提到的那样,goroutines起源于Communicating Sequential Processes,这是一种“用于描述并发系统中交互模式的形式语言”,根据定义,这可能意味着很多:)

我将基于core.async给出示例,因为我比Goroutines更了解它的内部。但是core.async是在Goroutines / CSP模型之后构建的,因此从概念上讲应该不会有太多差异。

core.async / Goroutine中的主要并发原语是一个channel。可以将achannel视为“岩石上的队列”。该通道用于“传递”消息。任何想“参与游戏”的过程都会创建或获取对a的引用,并向其channel发送/接收(例如发送/接收)消息。

免费24小时停车

在通道上完成的大部分工作通常都在“ Goroutine ”或“ go block ”内部进行,“ Goroutine ”或“ go block ”会拿住它的主体并检查它是否进行了任何通道操作。它将主体变成一个状态机。状态机将被“停放”,并释放实际的控制线程。这种方法类似于C#异步中使用的方法。当阻塞操作完成时,代码将恢复(在线程池线程上,或者在JS VM中的唯一线程) ”()。

用视觉传达要容易得多。阻塞的IO执行如下所示:

阻止IO

您可以看到线程主要花费时间在等待工作上。这是相同的工作,但通过“ Goroutine” /“ go block”方法完成:

核心异步

这里有2个线程完成了所有工作,而4个线程以阻塞方式完成了所有工作,同时花费了相同的时间。

上面描述中的关键是:没有工作时“线程被停放”,这意味着它们的状态被“卸载”到状态机,并且实际的实时JVM线程可以自由地做其他工作(源于出色的视觉效果)

注意:在core.async中,可以在“ go block”之外使用通道,通道将由JVM线程支持,而没有停放能力:例如,如果它阻塞,它将阻塞实际线程。

转到频道的力量

“ Goroutines” /“ go blocks”中的另一个重要之处是可以在通道上执行的操作。例如,可以创建一个超时通道,该通道将在X毫秒内关闭。或选择/替代!当与许多渠道结合使用时,该功能就像跨不同渠道的“您准备好了”轮询机制一样。可以将其视为非阻塞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))))))

此代码段来自wracer,在其中将相同的请求发送到所有三个:Yahoo,Bing和Google,并从最快的一个返回结果,或者如果在给定时间内没有返回结果则超时(返回超时消息)。Clojure的可能不是你的第一语言,但你可以不同意就如何顺序此实现并发的外观和感觉的。

您还可以合并/散入/散出来自/到许多通道的数据,映射/减少/过滤/ ...通道数据等等。频道也是头等公民:您可以将频道传递给频道。

去UI去!

由于core.async“执行块”具有“执行”状态的“能力”,并且在处理并发时具有非常连续的“外观”,因此JavaScript呢?JavaScript中没有并发,因为只有一个线程,对吗?并发的模仿方式是通过1024个回调。

但这不必是这种方式。上面来自wracer的示例实际上是用ClojureScript编写的,可编译为JavaScript。是的,它将在具有多个线程的服务器上和/或在浏览器中运行:代码可以保持不变。

Goroutines vs.core.async

再次,在实现上存在一些差异[还有更多]强调了以下事实:理论概念在实践中并不完全一对一:

  • 在Go中,输入的是通道,而在core.async中则不是:例如,在core.async中,您可以将任何类型的消息放在同一通道中。
  • 在Go中,您可以将可变的内容放在频道上。不建议这样做,但是可以。通过Clojure设计,在core.async中,所有数据结构都是不可变的,因此通道内的数据对其安全性而言要安全得多。

那么判决是什么?


我希望以上内容可以阐明参与者模型和CSP之间的差异。

并不是要引起大战,而是要给您另一个视角,例如Rich Hickey:

我对参与者仍然不热心。他们仍然将生产者与消费者结合在一起。是的,一个人可以与参与者模仿或实现某些队列(尤其是人们经常这样做),但是由于任何参与者机制已经包含了队列,因此队列似乎更原始了。应该注意的是,Clojure的状态并发使用机制仍然可行,并且通道面向系统的流量方面。 ”((

但是,实际上,Whatsapp是基于Erlang OTP的,并且似乎卖得很好。

另一个有趣的报价来自Rob Pike:

缓冲将不予确认发件人和可以任意长拿。缓冲通道,够程是非常接近的角色模型。

演员模型和Go的真正区别在于渠道是一等公民。同样重要的是:它们是间接的,就像文件描述符而不是文件名一样,允许并发样式在actor模型中不那么容易表达。在某些情况下,情况相反。我没有做出价值判断。理论上,模型是等效的。“(来源


2
哦,阿纳托利 您至少可以做的是正确拼写我的名字。:)
杰米

3
关于里奇·希基(Rich Hickey)的话,我认为队列的原始性不是对演员不热心的好理由。指针比引用更原始,但是我们没有为JVM设计的指针。
lcn 2014年

1
你好 Scala程序员已有5年了。Akka用户使用的时间大致相同。我使用演员的次数越多,我的热情就越低。当过度使用它们时,用它们创建非常复杂,难以理解的系统非常容易。演员在某些方面做得非常好。在大多数情况下,使用IMO的参与者通常会在一个简单的事件循环中表现良好或更好。
蒂姆·哈珀

8

将我的一些评论移至答案。:D太久了(不要忘了jamie和tolitius的帖子;它们都是非常有用的答案。)

可以使用Akka中的goroutine进行完全相同的操作并不是完全正确的。Go通道通常用作同步点。您不能直接在Akka中复制它。在Akka中,后同步处理必须移到一个单独的处理程序中(用jamie的话:D表示“ strewn”)。我会说设计模式是不同的。您可以使用来启动goroutine chan,进行一些处理,然后<-等待其完成再继续。Akka的形式不那么强大ask,但askIMO并不是Akka的。

还会键入Chan,而不会键入邮箱。这对IMO来说意义重大,对于基于Scala的系统而言,这非常令人震惊。我知道become很难用类型化的消息来实现,但也许表明这become与Scala不太相似。我可以大致说一下Akka。通常感觉就像自己的东西恰好在Scala上运行一样。Goroutine是Go存在的关键原因。

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

但是容错确实是Akka IMO的重点。您碰巧与此并发。并发是goroutine的核心。容错是Go中的另一件事,委托给deferrecover,可以用来实现很多容错功能。Akka的容错性较为正式且功能丰富,但也可能更加复杂。

所有人都说,尽管有一些相似之处,但Akka并不是Go的超集,并且它们在功能上有很大的差异。Akka和Go在鼓励您解决问题的方式上有很大的不同,一种简单的事情在另一种中很尴尬,不切实际或至少是非惯用的。这就是任何系统中的关键区别因素。

因此,将其带回到您的实际问题:我强烈建议您在将其引入Scala或Akka(与IMO完全不同的东西)之前重新考虑Go接口。确保您按照目标环境执行操作的方式进行操作。复杂的Go库的直接端口很可能不适用于任何一种环境。


7

这些都是伟大而彻底的答案。但是,以一种简单的方式查看它,这是我的观点。Goroutines是Actor的简单抽象。Actor只是Goroutines的一个更特定的用例。

您可以通过在通道旁边创建Goroutine来使用Goroutines实现Actor。通过确定该通道是该Goroutine的“所有者”,您就是说只有Goroutine会从中使用它。您的Goroutine只需在该通道上运行收件箱消息匹配循环。然后,您可以简单地将频道作为“演员”(Goroutine)的“地址”传递。

但是由于Goroutines是抽象的,是比Actor更通用的设计,因此Goroutines可以用于比Actor更多的任务和设计。

不过,要权衡一下,因为Actor是更具体的情况,所以像Erlang这样的actor的实现可以更好地优化它们(inbox循环上的rail递归),并且可以更轻松地提供其他内置功能(多进程和机器actor) 。


2

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

在“转到”频道中,您可以向该频道发送消息,并且可以收听任意数量的收件人,并且其中一个将接收到该消息。

在Actor中,只有您向其actor-ref发送消息的actor会收到该消息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.