【问题标题】:Is Scala's actors similar to Go's coroutines?Scala 的 actor 是否类似于 Go 的协程?
【发布时间】:2014-03-24 22:01:03
【问题描述】:

如果我想移植一个使用 Goroutines 的 Go 库,Scala 是否是一个不错的选择,因为它的收件箱/akka 框架在本质上类似于协程?

【问题讨论】:

  • Clojure 的 core.async 库可能比 akka 更适合 goroutine。除此之外,您的回答很大程度上取决于开发人员愿意使用/学习的内容。
  • @Dan 不是很主观,我正在寻找一个 1:1 的功能比较,所以移植是不费吹灰之力的,因为它反对重写库 b/c 语言差异是如此巨大。但你可能有一点……
  • 您可以查看 github.com/rssh/scala-gopher 以了解 scala 中类似 go 的 CSP 原语。

标签: scala go


【解决方案1】:

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

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

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

Actor 还有另一个非常好的特性——容错。通过按照 Erlang 设计的 OTP 规范将参与者组织到监督层次结构中,您可以在应用程序中构建故障域。就像值类/DTO/无论你想怎么称呼它们一样,你可以对失败进行建模,它应该如何处理以及在层次结构的哪个级别。这非常强大,因为您在 CSP 内部几乎没有故障处理能力。

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

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

祝你好运!

【讨论】:

  • +0.75 :) 我认为“不”太强了。在消息传递的意义上存在相似之处。细节和功能不同,但最终目标非常相似。两者都试图通过消息传递来解决并发编程问题。
  • 这不无道理。它们都是异步的,尽管频道生产者与频道消费者的连接点在时间上比演员的“一劳永逸”模型更加耦合。它们都是基于消息的。在我看来,Actor 非常适合容错和跨节点扩展,其中 CSP 是利用节点内的多个线程的有效机制。
  • @user1361315,你可以用 Akka 做同样的事情并不完全正确。 Go 通道通常用作同步点。你不能直接在 Akka 中复制它。在 Akka 中,必须将后同步处理移到单独的处理程序中(用 jamie 的话来说是“散布”:D)。我会说设计模式是不同的。你可以用一个 chan 启动一个 goroutine,做一些事情,然后 <- 等待它完成后再继续。 Akka 使用 ask 的这种形式不太强大,但 ask 并不是真正的 Akka 方式 IMO。 Chans 也会被输入,而邮箱则不会。
  • @jamie 所以 Akka 实际上是被动反应的:programmers.stackexchange.com/q/255047/13154 ?
  • 正在阅读这个想法“嗯,这很易读,但对理论和实践都有很强的理解。”然后我注意到了署名——“啊。这就解释了”
【解决方案2】:

这里有两个问题:

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

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

当然有很多意见关于为什么 Scala 更好或更差的原因作为一种语言(例如 here 是我的),但这些只是意见,不要不要让他们阻止你。 由于 Scala 是通用的,它“几乎”归结为:你可以用 X 语言做的所有事情,你都可以用 Scala 做。如果听起来太宽泛.. continuations in Java 怎么样 :)

  • Scala actor 是否类似于 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 垃圾收集、类加载、进程调度等。

上面的意思并不是说一个比另一个更好,而是表明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 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))))))

这段代码 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)

【讨论】:

  • 哦,阿纳托利。你至少可以把我的名字拼错。 :)
  • 关于 Rich Hickey 的引用,我不认为队列的原始性是对演员不热情的一个很好的理由。指针比引用更原始,但我们没有为 JVM 设计的指针。
  • 嗨。 Scala 程序员 5 年了。 Akka 用户的时间大致相同。我使用演员的次数越多,我就越不热情。当过度使用它们时,用它们创建一个非常复杂、难以理解的系统是非常容易的。有些事情演员做得非常好。在大多数情况下,IMO 会在简单的事件循环可以正常工作或更好的情况下使用演员。
【解决方案3】:

将我的一些 cmets 移动到答案中。它变得太长了:D(不要从 jamie 和 tolitius 的帖子中删除;它们都是非常有用的答案。)

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

还输入了频道,而邮箱则没有。这对 IMO 来说意义重大,对于基于 Scala 的系统来说,这非常令人震惊。我知道become 很难用键入的消息来实现,但这可能表明become 不是很像Scala。对于 Akka,我一般可以这么说。它通常感觉像是在 Scala 上运行的自己的东西。 Goroutines 是 Go 存在的一个关键原因。

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

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

总而言之,尽管有一些相似之处,但 Akka 不是 Go 的超集,它们在功能上存在显着差异。 Akka 和 Go 在鼓励你解决问题的方式上大相径庭,在一个方面很容易的事情在另一个方面是笨拙的、不切实际的,或者至少是不习惯的。这是任何系统的关键区别。

所以回到你的实际问题:我强烈建议在将 Go 接口引入 Scala 或 Akka 之前重新考虑它(这也是 IMO 完全不同的东西)。确保您按照目标环境的方式进行操作。一个复杂的 Go 库的直接移植可能不适合这两种环境。

【讨论】:

    【解决方案4】:

    这些都是伟大而彻底的答案。但是对于一个简单的看待它的方法,这是我的观点。 Goroutines 是 Actor 的简单抽象。 Actor 只是 Goroutine 的一个更具体的用例。

    您可以通过在 Channel 旁边创建 Goroutine 来使用 Goroutine 实现 Actor。通过决定该通道由该 Goroutine “拥有”,您是在说只有该 Goroutine 会从中消费。你的 Goroutine 只是在那个 Channel 上运行一个收件箱消息匹配循环。然后,您可以简单地将 Channel 作为“Actor”(Goroutine)的“地址”传递。

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

    不过,一个折衷方案是,由于 Actors 是一个更具体的案例,像 Erlang 这样的 Actors 的实现可以更好地优化它们(收件箱循环上的轨道递归),并且可以更容易地提供其他内置功能(多进程和机器演员)。

    【讨论】:

      【解决方案5】:

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

      在 Go 频道中,您向频道发送消息,并且可以有任意数量的接收者在收听,其中一个会收到消息。

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

      【讨论】:

        猜你喜欢
        • 2011-04-12
        • 2013-02-25
        • 1970-01-01
        • 1970-01-01
        • 2011-05-16
        • 2010-12-30
        • 1970-01-01
        • 2016-08-02
        • 2021-12-27
        相关资源
        最近更新 更多