1. 1.2 Actor 模型的核心概念
上一节说明为何值得考虑 Actor。本节只谈 Actor 模型里通用的概念,暂不绑定具体语言或框架;与本书实现 zhenyi 的对应关系集中在 1.2.9。
1.1. 1.2.1 Actor 是什么
Actor 常概括为三部分的组合:
Actor = 状态 + 行为 + 邮箱
| 组成 | 说明 | 辅助理解 |
|---|---|---|
| 状态 | 仅 Actor 内部可见的数据 | 外部不能直接改写 |
| 行为 | 收到消息后如何响应,可随状态变化 | 与「处理逻辑」同义 |
| 邮箱 | 缓冲传入消息的队列,通常按序消费 | 保证入队顺序与处理顺序的约定 |
三者一起构成一个边界明确的计算单元。
1.1.1. 与「对象」的常见对比
对象:
obj.field = value ← 外部可直接改状态
obj.method() ← 同步调用,当前栈上返回
Actor:
tell(actorRef, msg) ← 通过发消息交互,不直接改对方状态
由 Actor 自行从邮箱取消息并处理 ← 投递与处理异步于调用方
粗略地说:对象侧重同步调用与共享可见字段;Actor 侧重异步消息与封装状态,外部只能通过消息间接驱动变化。
1.2. 1.2.2 邮箱(Mailbox):顺序保证
消息先进入邮箱,再由 Actor 逐条取出处理。
消息 1 → [邮箱] → Actor 处理消息 1
消息 2 → [邮箱] → Actor 处理消息 2
消息 3 → [邮箱] → Actor 处理消息 3
在单邮箱、单消费线程的模型下:同一时刻,一个 Actor 只处理一条消息。
由此带来的直接好处包括:
- 减少锁:状态只在处理线程上更新(在 Go 中常对应一条 goroutine)。
- 顺序可预期:排除实现另做广告的前提下,处理顺序与入队约定一致。
- 推理路径短:核心逻辑可近似按单线程阅读。
1.2.1. 邮箱的容量与背压
工程上常见两类:
- 有界邮箱:队列满时,发送方阻塞或得到失败,背压显式作用在入队。
- 无界邮箱:入队通常不因「满」而阻塞,队列长度在突发下增长,背压体现在内存与延迟上,常需业务侧限流、降级或扩容。
具体语义取决于运行时;不宜把某一种实现当成 Actor 模型的固定公理。
1.3. 1.2.3 位置透明性
文献中常强调的理想是:向本地或远端 Actor 发送消息时,调用形态尽量一致。
tell(targetRef, msg)
// 由运行时解析 targetRef:本地则入队,远端则经网络发送
调用方理想上只面对统一的「引用」抽象;实际中远端路径有更长的延迟和更高的失败率;至多一次、至少一次等语义由传输与中间件决定,不是 Actor 一词本身所能担保。延迟、错误处理与可观测性仍需按分布式系统常规范式设计。
1.4. 1.2.4 监督(Supervision)
Actor 可能因崩溃、超时或逻辑错误而终止。监督描述的是:终止之后由谁、按何种策略恢复或上报。
子 Actor 异常退出
↓
监督者获知
↓
常见策略:重启 / 停止 / 上报上级监督者
在 Erlang/OTP 等体系中,监督树较深、与海量进程配套。在 Actor 数量较少 的部署形态(见 1.3 节 中的服务级做法)里,常见扁平监督:少量监督者对等业务 Actor,配以重启间隔与次数上限,抑制故障抖动。
具体重启次数、计数是否按时间窗口清零等,由各框架定义;概念上只需记住:生命周期与故障恢复是可配置的,而非无限自旋。
1.5. 1.2.5 行为切换
部分系统支持显式切换消息处理行为(类似「下一段消息改走另一套规则」)。更常见的实现是不显式改 API,而维护内部阶段,用分支或模式匹配区分:
// 示意:状态机,非特定框架 API
func (a *chatActor) onMessage(msg Msg) {
switch a.phase {
case phaseLogin:
a.handleLogin(msg)
case phaseChat:
a.handleChat(msg)
}
}
两种思路在表达能力上往往接近,差别主要在平台是否提供语法层面的便利。
1.6. 1.2.6 Actor 的生命周期
概念上可概括为(各实现命名不同):
创建 / 注册
↓
初始化(资源、配置、依赖)
↓
运行:从邮箱取消息 → 调用当前行为处理
↓ 正常关闭请求
尽量排空邮箱(严格程度因实现而异)
↓
退出
↓ 异常退出时由监督策略决定是否重启、从哪一步恢复
本书 第三章 将结合 zhenyi,把上述阶段对应到 Init、Run、Close 等符号。
1.7. 1.2.7 消息传递的模式
1.7.1. Fire-and-Forget(告知)
tell(target, message)
// 发送方一般不等待处理结果
适用于通知、事件扩散等不必同步等待结果的场景。
1.7.2. Request-Response(请求-答复)
reply := ask(target, request, timeout)
// 语义上等待答复;具体阻塞线程或协程由实现决定
适用于查询、命令-答复式 RPC 等需要返回结果的场景。
结构上的注意点:若在当前正在处理某条消息的同一条执行路径上同步阻塞等待答复,而答复又必须经同一路径才能被处理,可能发生自我死锁。常见规避方式包括:换工作线程、异步回调、或非阻塞请求 API。本书第三章结合 zhenyi 的 CallActor 会再讨论。
1.7.3. 选型简表
| 模式 | 延迟 | 可靠性 | 适用场景 |
|---|---|---|---|
| Fire-and-Forget | 低 | 可能丢 | 通知、日志、事件 |
| Request-Response | 较高 | 可超时、可重试 | 查询、RPC |
1.8. 1.2.8 本节要点(模型层)
状态、行为、邮箱构成 Actor;邮箱上的顺序处理减轻对锁的依赖。位置透明是设计目标,分布式下仍需单独处理延迟、失败与消息语义。监督与生命周期由策略与框架共同约定。告知与请求-答复适用场景不同;后者需注意避免在处理路径上同步自锁。
1.9. 1.2.9 附录:概念与 zhenyi 的对应
以下为 1.2.1~1.2.8 中术语在 zhenyi 中的常见落点(以源码为准):
| 概念 | zhenyi 中常见落点 |
|---|---|
| 邮箱 | zhenyi-base/zqueue.UnboundedMPSC;背压与监控见 第三章、第六章 |
| tell | SendActor、SendMsg 等 |
| ask | CallActor(避免阻塞 Actor 主处理路径;常见配合 AsyncRun 等) |
| 远端消息路径 | 总线、服务发现、路由策略(第四、五章) |
| 监督 | Group.superviseActor,及 ActorConfig.MaxRestarts、重启窗口等(第三章) |
1.2.1~1.2.8 可作为与实现无关的导读;1.2.9 便于在阅读仓库时按表查阅。