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,把上述阶段对应到 InitRunClose 等符号。

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 SendActorSendMsg
ask CallActor(避免阻塞 Actor 主处理路径;常见配合 AsyncRun 等)
远端消息路径 总线、服务发现、路由策略(第四、五章
监督 Group.superviseActor,及 ActorConfig.MaxRestarts、重启窗口等(第三章

1.2.1~1.2.8 可作为与实现无关的导读;1.2.9 便于在阅读仓库时按表查阅。

results matching ""

    No results matching ""