Actor模型解决了什么问题?
由于CPU的工艺制程和发热稳定性之间难以取舍,取而代之的策略是增加CPU核心的数量。多核处理器应运而生,计算处理变成了团队协作,效率的提升通过多个核心的通信来实现,而不是传统的时钟速度的提升。这也是线程发挥作用的地方。目前家用PC四核已经非常常见,服务器更是达到32核64线程。为了高效的利用多核CPU,应该在代码层面就考虑并发性。
一般来说有两种策略用来在并发线程中进行通信:共享数据和消息传递。
-
使用共享数据方式的并发编程面临的最大的一个问题就是数据条件竞争(data race)。处理各种锁的问题是让人十分头痛的一件事。
-
消息传递机制和共享数据方式相比最大的优点就是不会产生数据竞争状态。实现消息传递有两种常见的类型:基于channel的消息传递和基于Actor的消息传递。本文主要是来分享Actor模型。
Actor模型是什么?
Actor模型是一个概念模型,用于处理并发计算。它定义了一系列系统组件应该如何动作和交互的通用规则,最著名的使用这套规则的编程语言是Erlang。
Actor由3部分组成:状态(State)+行为(Behavior)+邮箱(Mailbox)
- State是指actor对象的变量信息,存在于actor之中,actor之间不共享内存数据,actor只会在接收到消息后,调用自己的方法改变自己的state,从而避免并发条件下的死锁等问题;
- Behavior是指actor的计算行为逻辑;邮箱建立actor之间的联系,一个actor发送消息后,接收消息的actor将消息放入邮箱中等待处理
- 邮箱内部通过队列实现,消息传递通过异步方式进行。
Actor模型描述了一组为避免并发编程的公理:
- 所有的Actor状态是本地的,外部是无法访问的。
- Actor必须通过消息传递进行通信
- 一个Actor可以响应消息、退出新Actor、改变内部状态、将消息发送到一个或多个Actor。
- Actor可能会堵塞自己但Actor不应该堵塞自己运行的线程
Actor是分布式存在的内存状态及单线程计算单元,一个Id对应的Actor只会在集群种存在一个(有状态的 Actor在集群中一个Id只会存在一个实例,无状态的可配置为根据流量存在多个),使用者只需要通过Id就能随时访问不需要关注该Actor在集群的什么位置。单线程计算单元又保证了消息的顺序到达,不存在Actor内部状态竞用问题。
可以把通信的线程可以想象成两个无法直接说话而必须通过邮件交流的人,双方要交流就要发送邮件。发送方邮件一旦发出就不能修改任何内容,而且是没有办法收回修改后再发的,这也就是消息一旦发出就不可改变
。对于接收方而言,想什么时候看邮件就什么时候看,而且不需要监听,这就叫异步
。接收方看了发送方的邮件可以回复也可以撒都不做。只是回复邮件一旦发出也同样是不能收回修改的,也就是不可变性两端都是一样的。同样,发送方针对回复邮件,也是想什么时候看就什么时候看。两端同样都是异步的。这种通信模型就是Actor想要的模型
,可以发现这种通信方式其实依赖一套邮件系统或叫做消息管理系统
。进程内部要有一套这样的系统,给每个线程一个独立的收发消息的管道,并且都是异步的。
Actor的缺点
当所有逻辑都跑在Actor中的时候,很难掌握Actor的粒度,稍有不慎就可能造成系统中Actor个数爆炸的情况。
当必须共享数据或状态时很难避免使用锁,由于Actor可能会堵塞自己但Actor不应该堵塞它运行的线程,此时也许可选择使用Redis做数据共享。
ActorLocation
基于Actor开发的架构,在给某个Actor发送消息时必须要知道Actor对象的地址,一旦Actor地址改变了(玩家进行了跨服,逻辑上就是用户Actor单位跳转了进程),那么消息就无法准确到达。为了解决这一现象,ActorLocation模型诞生。
ActorLocation相比Actor多了一个用于注册Actor位置的第三方ActorLocationSever,所有Actor消息的发送都要经由第三方转发。在普通Actor改变地址时(进行跨服跳转)必须注册上报Actor新的地址。由于Actor的唯一ID是不会改变的,那么后续发送的消息可以更具这个唯一ID找到Actor对应的新地址,然后转发。
ET的actor跟actor location的比喻
中国有很多城市(进程),城市中有很多人(entity对象)居住,每个人都有身份证号码(Entity.Id)。一个人每到一个市都需要办理居住证,分配到唯一的居住证号码(InstanceId),居住证号码的格式是2个字节市编号+4个字节时间+2个字节递增。身份证号码是永远不会变化的,但是居住证号码每到一个城市都变化的。 现在有个中国邮政(actor)。假设小明要发信给女朋友小红
- 小红为了收信,自己必须挂载一个邮箱(MailboxComponent),小红收到消息就会处理。注意这里处理是一个个进行处理的。有可能小红会同时收到很多人的信。但是她必须一封一封的信看,比方说小明跟小宝都发了信给小红,小红先收到小明的信,再收到了小宝的信。小红先读小明的信,小明信中让小红给外婆打个电话(产生协程)再给自己回信,注意这期间小红也不能读下一封信,必须打完电话后才能读小宝的信。当然小红自己可以选择不处理完成就开始读小宝的信,做法是小红开一个新的协程来处理小明的信。
- 假设小明知道小红的居住证号码,那么邮政(actor)可以根据居住证号码头两位找到小红居住的城市(进程),然后再根据小红的居住证编号,找到小红,把消息投递到小红的邮箱(MailboxComponent)中。这种是最简单的原生的actor模型
- ET还支持了一套actor location机制。假设小明不知道小红的居住证号码,但是他知道小红的身份证号码,怎么办呢?邮政开发了一套高级邮政(actor location)想了一个办法,如果一个人经常搬家,它还想收到信,那他到一个新的城市都必须把自己的居住证跟身份证上报到中央政府(location server),这样高级邮政能够通过身份证号码来发送邮件。方法就是去中央政府拿到小红的居住证号码,再利用actor机制发送。
- 假设小红之前在广州市,小明用小红的身份证给小红发信件了。 高级邮政获取了小红的居住证号码,给小红发信。发信的这个过程中,小红搬家了,从广州搬到了深圳,这时小红在中央政府上报了自己新的居住证。 高级邮政的信送到到广州的时候发现,小红不在广州。那么高级邮政会再次去中央政府获取小红的居住证,重新发送,有可能成功有可能再次失败,这个过程会重复几次,如果一直不成功则告诉小明,信件发送失败了。
- 高级邮政发信比较贵,而且人搬家的次数并不多,一般小明用高级邮政发信后会记住小红的居住证,下次再发的时候直接用居住证发信,发送失败了再使用高级邮政发信。
- 高级邮政的信都是有回执的,有两种回执,一种回执没有内容,只表示小红收到了信,一种回执带了小红的回信。小明在发信的时候可以选择使用哪种回执形式。小明给小红不能同时发送两封信,必须等小红的回执到了,小明才能继续发信。