源于从Erlang到Go的一些思惟碰撞,就像当初从C++到Erlang同样,整理下来记于此。git
Actor
Actor模型,又叫参与者模型,其”一切皆参与者(actor)”的理念与面向对象编程的“一切皆是对象”相似,可是面向对象编程中对象的交互一般是顺序执行的(占用的是调用方的时间片,是否并发由调用方决定),而Actor模型中actor的交互是并行执行的(不占用调用方的时间片,是否并发由本身决定)。github
在Actor模型中,actor执行体是第一类对象,每一个actor都有本身的ID(类比人的身份证),能够被传递。actor的交互经过发送消息来完成,每一个actor都有一个通讯信箱(mailbox,本质上是FIFO消息队列),用于保存已经收到但还没有被处理的消息。actorA要向actorB发消息,只需持有actorB ID,发送的消息将被当即Push到actorB的消息信箱尾部,而后返回。所以Actor的通讯原语是异步的。编程
从actor自身来讲,它的行为模式可简化为:并发
- 发送消息给其它的actor
- 接收并处理消息,更新本身的状态
- 建立其它的actor
一个好的Actor模型实现的设计目标:框架
- 调度器: 实现actor的公平调度
- 容错性: 具有良好的容错性和完善错误处理机制
- 扩展性: 屏蔽actor通讯细节,统一本地actor和远程actor的通讯方式,进而提供分布式支持
- 热更新? (还没弄清楚热更新和Actor模型,函数式范式的关联性)
在Actor模型上,Erlang已经耕耘三十余载,以上提到的各个方面都有很是出色的表现,其OTP整合了在Actor模型上的最佳实践,是Actor模型的标杆。异步
CSP
顺序通讯进程(Communicating sequential processes,CSP)和Actor模型同样,都由独立的,并发的执行实体(process)构成,执行实体间经过消息进行通讯。但CSP模型并不关注实体自己,而关注发送消息使用的通道(channel),在CSP中,channel是第一类对象,process只管向channel写入或读取消息,并不知道也不关心channel的另外一端是谁在处理。channel和process是解耦的,能够单首创建和读写,一个process能够读写(订阅)个channel,一样一个channel也可被多个process读写(订阅)。分布式
对每一个process来讲:函数
- 从命名channel取出并处理消息
- 向命名channel写入消息
- 建立新的process
Go语言并无彻底实现CSP理论(参见知乎讨论),只提取了CSP的process和channel的概念为并发提供理论支持。目前Go已是CSP的表明性语言。post
CSP vs Actor
- 相同的宗旨:”不要经过共享内存来通讯,而应该经过通讯来共享内存”
- 二者都有独立的,并发执行的通讯实体
- Actor第一类对象为执行实体(actor),CSP第一类对象为通讯介质(channel)
- Actor中实体和通讯介质是紧耦合的,一个Actor持有一个Mailbox,而CSP中process和channel是解耦的,没有从属关系。从这一层来讲,CSP更加灵活
- Actor模型中actor是主体,mailbox是匿名的,CSP模型中channel是主体,process是匿名的。从这一层来讲,因为Actor不关心通讯介质,底层通讯对应用层是透明的。所以在分布式和容错方面更有优点
Go vs Erlang
- 以上 CSP vs Actor
- 均实现了语言级的coroutine,在阻塞时能自动让出调度资源,在可执行时从新接受调度
- go的channel是有容量限制的,所以只能必定程度地异步(本质上仍然是同步的),erlang的mailbox是无限制的(也带来了消息队列膨胀的风险),而且erlang并不保证消息是否能到达和被正确处理(但保证消息顺序),是纯粹的异步语义,actor之间作到彻底解耦,奠基其在分布式和容错方面的基础
- erlang/otp在actor上扩展了分布式(支持异质节点),热更和高容错,go在这些方面还有一段路要走(受限于channel,想要在语言级别支持分布式是比较困难的)
- go在消息流控上要作得更好,由于channel的两个特性: 有容量限制并独立于goroutine存在。前者能够控制消息流量并反馈消息处理进度,后者让goroutine自己有更高的处理灵活性。典型的应用场景是扇入扇出,Boss-Worker等。相比go,erlang进程老是被动低处理消息,若是要作流控,须要本身作消息进度反馈和队列控制,灵活性要差不少。另一个例子就是erlang的receive操做须要遍历消息队列(参考),而若是用go作同步调用,经过单独的channel来作则更优雅高效
Actor in Go
在用Go写GS框架时,不自觉地会将goroutine封装为actor来使用:设计
- GS的执行实体(如玩家,公会)的逻辑具有强状态和功能聚合性,不易拆分,所以一般是一个实体一个goroutine
- 实体接收的逻辑消息具有弱优先级,高顺序性的特色,所以一般实体只会暴露一个Channel与其它实体交互(结合go的interface{}很容易统一channel类型),这个channel称为RPC channel,它就像这个goroutine的ID,几乎全部逻辑goroutine之间经过它进行交互
- 除此以外,实体还有一些特殊的channel,如定时器,外部命令等。实体goroutine对这些channel执行select操做,读出消息进行处理
- 加上goroutine的状态数据以后,此时的goroutine的行为与actor类似:接收消息(多个消息源),处理消息,更新状态数据,向其它goroutine发送消息(经过RPC channel)
到目前为止,goroutine和channel解耦的优点并未体现出来,我认为主要的缘由仍然是GS执行实体的强状态性和对异步交互流程的顺序性致使的。
在研究这个问题的过程当中,发现已经有人已经用go实现了Actor模型: https://github.com/AsynkronIT/protoactor-go。 支持分布式,甚至supervisor,总体思想和用法和erlang很是像,真是有种他山逢知音的感受。:)
参考:
- http://jolestar.com/parallel-programming-model-thread-goroutine-actor/
- https://www.zhihu.com/question/26192499
http://wudaijun.com/2017/05/go-vs-erlang/