Scala的参与者与Go语言的协程相似吗?

cbwuti44  于 2022-12-26  发布在  Scala
关注(0)|答案(5)|浏览(203)

如果我想移植一个使用Goroutine的Go类库,Scala会是一个好的选择吗?因为它的inbox/akka框架在本质上与协程相似。

643ylb08

643ylb081#

不,它们不是,Goroutine是基于TonyHoare在1978年提出的通信顺序进程理论,其思想是可以有两个进程或线程彼此独立地运行,但共享一个“通道”。“一个进程/线程将数据放入其中,另一个进程/线程使用数据。最突出的实现是Go语言的通道和Clojure”s core.async,但此时它们仅限于当前运行时,并且不能分发,即使在同一物理盒上的两个运行时之间也是如此。
CSP已经发展到包含了一个静态的、形式化的进程代数来证明代码中存在死锁。这是一个非常好的特性,但是Goroutines和core.async目前都不支持它。如果它们支持它,那么在运行代码之前知道死锁是否可能是非常好的。然而,CSP并没有以一种有意义的方式支持容错。因此,作为开发人员,您必须弄清楚如何处理可能发生在通道两端的故障,而这样的逻辑最终会散布在整个应用程序中。
休伊特在1973年指定的参与者涉及具有自己邮箱的实体,它们本质上是异步的,并且具有跨越运行时和计算机的位置透明性--如果您有引用的话( akka 语)或PID(Erlang),你可以用消息发送它。这也是一些人在基于Actor的实现中发现错误的地方,因为您必须引用其他参与者才能向其发送消息,从而直接耦合发送者和接收者。在CSP模型中,通道是共享的,可以由多个生产者和消费者共享。根据我的经验,这并不是一个大问题。2我喜欢代理引用的概念,这意味着我的代码不会充斥着如何发送消息的实现细节--我只是发送一个消息,无论参与者位于何处,它都会接收到它。3如果那个节点发生故障,参与者在其他地方转世,理论上对我来说是透明的。
Actors还有另一个非常好的特性-容错。通过按照Erlang中设计的OTP规范将actors组织到一个监督层次结构中,您可以在应用程序中构建一个故障域。就像值类/DTO/无论您想如何称呼它们一样,您可以对故障建模,应该如何处理以及在层次结构的哪个级别。这是非常强大的。因为CSP内部几乎没有故障处理能力。
参与者也是一个并发范例,其中参与者可以在其内部具有可变状态,并保证不会对该状态进行多线程访问,除非构建基于参与者的系统的开发人员意外地引入了它,例如将参与者注册为回调的侦听器,或者通过Futures在参与者内部进行异步。
无耻插件--我正在和Akka团队的负责人罗兰Kuhn一起写一本新书,叫做React式设计模式,在书中我们讨论了所有这些以及更多。绿色线程、CSP、事件循环、迭代对象、React式扩展、参与者、未来/承诺等等。预计在下月初会看到关于曼宁的MEAP。

n3h0vuf2

n3h0vuf22#

这里有两个问题:

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

这是一个简单的问题,因为Scala是一种通用语言,它并不比您可以选择“移植goroutine”的许多其他语言更好或更差。
当然,对于Scala作为一种语言的优劣,有很多观点(例如,here是我的观点),但这些只是观点,不要让它们阻止你。由于Scala是通用的,它“几乎”可以归结为:你能用X语言做的一切,你都能用Scala做。如果听起来太宽泛...... continuations in Java怎么样:)

  • Scala参与者与goroutines类似吗?

唯一的相似之处(除了吹毛求疵之外)是它们都与并发和消息传递有关,但相似之处也就到此为止了。
由于Jamie的回答很好地概述了Scala actor,所以我将更多地关注Goroutines/core.async,但会介绍一些actor模型。

演员助力事情“无忧配送”

“无忧”产品通常与以下术语相关:fault toleranceresiliencyavailability等等。
我们不去深入研究演员如何工作的细节,简单地说,演员必须做的两件事:

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

考虑“对话进程”,其中每个进程都有一个引用和一个在消息到达时被调用的函数。
当然还有更多的内容(例如,查看Erlang OTPakka docs),但以上两个是一个很好的开始。
参与者最有趣的地方是..实现。目前,Erlang OTP和Scala AKKA是两个最大的参与者。虽然它们都致力于解决相同的问题,但也有一些不同之处。让我们看一下其中的两个:

  • 我有意不使用诸如“引用透明性”、“幂等性”等术语,它们除了引起混乱之外没有任何好处,所以我们只谈不变性(一个can't change that概念)。Erlang作为一种语言是固执己见的,它倾向于强不变性,而在Scala中,太容易让actor在接收消息时改变/变异它们的状态。不推荐,但是Scala中的可变性就摆在您面前,而且人们确实在使用它。
  • Joe Armstrong谈到的另一个有趣的观点是Scala/AKKA受到JVM的限制,JVM在设计时并没有真正考虑到“分布式”,而Erlang VM则考虑到了这一点。进程隔离、每个进程与整个VM垃圾收集、类加载、进程调度等。

上面的要点并不是说一个比另一个好,而是要表明参与者模型作为一个概念的纯度取决于它的实现。
现在来看看goroutine。
Goroutine帮助顺序推理并发性
正如前面提到的其他答案一样,goroutine起源于Communicating Sequential Processes,这是一种“描述并发系统中交互模式的形式化语言”,根据定义,它几乎可以表示任何东西:)
我将给予基于core.async的例子,因为我比Goroutines更了解它的内部结构,但是core.async是在Goroutines/CSP模型之后构建的,所以在概念上应该没有太多的区别。
core.async/Goroutine中主要的并发原语是channel,可以把channel想象成一个“岩石上的队列”,这个通道用来“传递”消息,任何想要“参与游戏”的进程都会创建或获取对channel的引用,并向它发送/从它接收消息。
24小时免费停车
在通道上完成的大部分工作通常发生在“Goroutine“或“go block”内,“”获取其主体并检查其是否有任何通道操作。它将主体转换为状态机。在到达任何阻塞操作时,状态机将被“暂停”,而实际的线程控制将被释放。2这种方法类似于C# async中使用的方法。当阻塞操作完成时,代码将被恢复(在线程池线程上,或者在JS VM中的唯一线程上)”(source)。
用视觉传达要容易得多。下面是阻塞IO执行的样子:

你可以看到线程大多数时间都在等待工作,下面是同样的工作,但是是通过“Goroutine”/“go block”方法完成的:

在这里,2个线程完成了阻塞方法中4个线程完成的所有工作,同时花费了相同的时间。
以上描述中的问题是:“threads areparking”(线程处于**暂停状态),当它们没有工作时,这意味着它们的状态被“卸载”到状态机,而实际活动的JVM线程可以自由地执行其他工作(source用于一个很好的可视化)

  • 注意 *:在core.async中,channel * 可以 * 在“go block“之外使用,它将由一个没有驻留能力的JVM线程支持:例如,如果它阻塞,则它阻塞真实的线程。

Go通道的力量

“Goroutines”/“go blocks”中另一个巨大的东西是可以在一个通道上执行的操作,例如可以创建一个超时通道,它将在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可能不是你的第一语言,但你不能不同意这种并发实现的外观和感觉是多么的连续
您还可以合并/扇入/扇出来自/到许多通道的数据,Map/减少/过滤/...通道数据等。你可以把一个频道传到另一个频道。

转到用户界面转到!

既然core.async的“go blocks”能够“驻留”执行状态,并且在处理并发时具有非常连续的“外观和感觉”,那么JavaScript呢?JavaScript中没有并发性,因为只有一个线程,对吗?而模拟并发性的方式是通过1024个回调。
但实际上并不一定要这样做,上面的wracer示例实际上是用ClojureScript编写的,它可以编译成JavaScript。是的,它可以在多线程服务器和/或浏览器中工作:代码可以保持相同。

Goroutine与核心异步

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

  • 在Go语言中,通道是有类型的,而在core.async中则不是:例如,在core.async中,你可以把任何类型的消息放在同一个通道上。
  • 在Go语言中,你可以把可变的东西放在通道上,虽然不推荐,但你可以这么做。在core.async中,Clojure设计的所有数据结构都是不可变的,因此通道中的数据会更安全。

那么结论是什么?

我希望上面的内容能对actor模型和CSP之间的差异有所帮助。
不是要引起一场激烈的战争,而是要给予你另一个视角,比如说里奇·希基:
“* 我仍然对参与者不感兴趣。他们仍然将生产者与消费者耦合在一起。是的,可以使用参与者模拟或实现某些类型的队列(值得注意的是,人们经常这样做),但是由于任何参与者机制都已经包含了队列,因此队列似乎更原始,应该注意的是,Clojure的并发使用状态机制仍然是可行的,通道朝向系统的流动方向。"(source
然而,在实践中,Whatsapp是基于Erlang OTP的,它似乎卖得很好。
另一个有趣的报价是从罗布派克:
缓冲发送不会向发送方确认,并且可能需要任意长的时间。缓冲通道和goroutine非常接近actor模型。*

  • 演员模型和Go语言的真实的区别在于渠道是一等公民,同样重要的是:它们是间接的,就像文件描述符而不是文件名,允许在参与者模型中不容易表达的并发样式。2也有相反的情况;我不是在做价值判断。理论上,这些模型是等效的。*"(source
thtygnil

thtygnil3#

  • 把我的一些评论移到一个答案上。它太长了:D(不是要从杰米和托里提乌斯的帖子中拿走;它们都是非常有用的答案。)*

在Akka中,你不可能做到和goroutine完全一样的事情。Go语言的通道经常被用作同步点。你不能直接在Akka中复制它。在Akka中,同步后的处理必须被转移到一个单独的处理程序中(jamie的原话是“strewn”:D)。我想说设计模式是不同的。你可以用chan启动一个goroutine,做一些事情,然后用<-等待它完成,然后再继续。Akka用ask做了一个功能较弱的形式,但ask并不是Akka的方式。
Chans也是类型化的,而邮箱不是。这是一个大问题,对于基于Scala的系统来说,这是相当令人震惊的。我知道become很难用类型化的消息实现。但也许这表明become并不十分像Scala。我可以这样说Akka。它经常感觉像是碰巧在Scala上运行的自己的东西。Goroutine是Go语言存在的一个关键原因。
别误会我的意思我非常喜欢演员模型,而且我总体上喜欢Akka,觉得在它上面工作很愉快。我总体上也喜欢Go语言(我觉得Scala很漂亮,而Go语言只是很有用;但它是相当有用的)。
但容错才是Akka IMO真正的重点,你碰巧会得到并发,并发是goroutine的核心,容错在Go语言中是一个独立的东西,被委托给deferrecover,它们可以用来实现相当多的容错,Akka的容错更正式,功能更丰富,但也可能更复杂。
总而言之,尽管Akka有一些短暂的相似之处,但它并不是Go语言的超集,而且它们在功能上有很大的差异。Akka和Go语言在如何鼓励你解决问题上有很大的不同,在一个系统中很容易的事情,在另一个系统中却很笨拙、不切实际,或者至少是不习惯的。这是任何系统的关键区别。
回到你真正的问题:我强烈建议在将Go语言移植到Scala或Akka之前重新考虑一下Go语言的接口(在IMO中,这两个东西也是完全不同的)。确保你是按照目标环境的方式来做事情的。一个复杂的Go语言库的直接移植很可能不适合任何一个环境。

n9vozmp4

n9vozmp44#

这些都是很好很彻底的答案,但为了简单地看待这个问题,我的观点是:Goroutine是Actors的一个简单抽象,Actors只是Goroutine的一个更具体的用例。
你可以通过在一个Channel旁边创建一个Goroutine来实现Actor。通过确定这个Channel是由那个Goroutine“拥有”的,你就可以说只有那个Goroutine会从它那里消费。你的Goroutine只是在那个Channel上运行一个收件箱消息匹配循环。然后你可以简单地把Channel作为你的“Actor”(Goroutine)的“地址”来传递。
但是,由于Goroutine是一种抽象,一种比actor更通用的设计,因此Goroutine可以用于比actor多得多的任务和设计。
但需要权衡的是,由于Actor是一种更具体的情况,像Erlang这样的Actor实现可以更好地优化它们(inbox循环上的rail递归),并可以更容易地提供其他内置特性(多进程和机器Actor)。

8mmmxcuj

8mmmxcuj5#

我们是否可以说,在Actor模型中,可寻址实体是Actor,即消息的接收者。2而在Go语言通道中,可寻址实体是通道,即消息流动的管道。
在Go通道中,您向通道发送消息,并且任意数量的接收者可以侦听,并且其中一个将接收消息。
在“执行元”中,只有一个执行元(您向其执行元引用发送消息)将接收消息。

相关问题