Actor Model in Action
系统介绍 Actor 模型:定义与公理、Shared-nothing、理论优势、语言生态;Erlang/OTP 与死锁(同步 call、超时、非自动拆环);对比 trpc-go、bingo stateful(含同步互调与死锁)、tsf4g-go 与横向对比;游戏应用;对象路由辨析;文末六条外部参考。
- 权威定义与三条公理
- 核心思想:解耦、封装与信箱
- Actor 模型要解决的核心问题
- 理论优势:可扩展性、容错与无界非确定性
- 主要使用场景
- 主流语言与生态(总览)
- trpc-go 中实现 Actor 模型的方法分析
- bingo 中实现 Actor 模型的方法分析
- tsf4g-go 中实现 Actor 模型的方法分析
- trpc-go、bingo、tsf4g-go 实现 Actor 模型方案的横向对比
- 游戏领域的具体应用
- Actor 模型与「对象路由」:概念辨析
- 局限与适用边界
- 小结
- 参考链接
Actor 模型是用于设计并发与分布式系统的数学理论与编程范式:将计算单元分解为独立的 Actor(演员),彼此之间仅通过 异步消息 通信与协作。该模型于 1973 年由 Carl Hewitt 等提出,旨在从模型层面化解传统并发编程(共享内存 + 锁)在复杂系统中的组合与演进难题;定义、历史与理论脉络见 [1]。
读本篇前可先记住三句话:(1)Actor 之间 不共享可变状态;(2)通信只靠 异步消息;(3)单 Actor 内对邮箱通常 顺序处理。其余都是这三条在工程上的展开。
与 CSP(如 Go 的 channel)关系:二者都强调「靠通信共享内存」,但 Actor 以「带地址的进程/对象 + 邮箱」为中心,CSP 以「通道上的同步/缓冲通信」为中心;实践中常混用或互相模拟;异同见 [1],下文 Go 节再展开。
权威定义与三条公理
[1] 将 Actor 模型描述为一种数学计算模型,其中 Actor 被当作并发计算的通用原语(primitive):系统由若干 Actor 组成,它们只通过消息相互影响。希望读更短的学术化节选时,可与 [3] 对照。
工程口语里常说「万物皆为 Actor」:每个 Actor 都是独立实体,一体封装 处理(Processing)、存储(即私有状态 / Storage) 与 通信(Communications)——外面只看见消息作为接口,内部用顺序逻辑演进状态与行为。
在经典表述(与英文维基及 Hewitt 一脉文献对齐)中,每收到一条消息,Actor 可做出的响应可归纳为三类操作(常概括为 send / create / become)。不少材料用「可并行发起」来概括这三类能力——强调的是彼此的独立性,而非多线程同时改写共享内存;具体列举如下:
- 向已知地址发送有限条消息(通常异步、非阻塞;是否有序投递依实现)。
- 创建有限个新 Actor。
- 指定接收下一条消息时的行为(即更新局部状态 / 行为,等价于顺序语义下的状态机步进,亦常称 become)。
同一轮响应中,多条「发送」「创建」在效果上还可与状态更新交错完成;具体语义仍以所读论文或运行时文档为准。
术语别称:「指定下一条消息的行为」有时也表述为 become(行为替换),与函数式语言中「不同模式匹配分支」的
receive相呼应。
Gul Agha 等后续工作将 Actor 提升为独立理论对象,讨论公平性、无界非确定性(unbounded nondeterminism)等——即全局可观察顺序不必由一把锁强行固定,而由异步与消息时序共同刻画,更贴近分布式现实;脉络见 [1]。
核心思想:解耦、封装与信箱
许多教材将 Actor 的核心概括为:在边界上把 状态(State)、行为(Behavior) 与 通信(以消息为唯一对外接口) 彻底分离——与上节「处理 / 存储 / 通信」三位一体表述同源:可变状态与处理逻辑留在 Actor 内部;Actor 与 Actor 之间看不到对方的内存,只看到异步消息。Shared-nothing 意味着 Actor 间不共享可变状态,从根源上削弱对业务层锁的依赖,缓解共享内存多线程的典型复杂性。[1] 给出公理化表述,[2] 侧重工程读者常问的术语与误解澄清,[6] 可作为中文辅助笔记与 [1][2] 交叉印证。
独立自治
每个 Actor 是独立计算单元,私有状态与行为逻辑对外不可被别线程直接篡改;外界只能通过发消息施加影响。
异步消息驱动
Actor 不共享可变状态;协作只靠异步消息。发送方发完后不必阻塞等待(除非在应用层再编一套「请求—回复」协议),从而在空间和时间上解耦通信双方。
信箱(Mailbox)
每个 Actor 配有 Mailbox(常见实现为 FIFO 队列)。到达的消息先入箱,再由运行时依次投递给该 Actor 的处理逻辑——在典型保证下,单 Actor 一次只处理一条消息,因而在该 Actor 内部仍可写顺序代码,避免业务层锁。跨 Actor 时,多个并发流之间的相对顺序通常不保证全局稳定,这与「消息可能经不同路径、不同延迟到达」的物理现实一致。
注意:「单线程处理邮箱」是常见实现契约,不是物理学定律;有些运行时为吞吐会做多队列或批处理优化,语义仍以文档与 API 为准。
局部性与位置透明
局部性:Actor 只直接知晓能发送消息的有限地址;系统通过逐级创建与传递引用组合而成。
地址与别名:形式化模型里,一个 Actor 可对应多个地址(别名),也可出现多 Actor 共享同一地址的设定(用于复制、负载均衡等),具体以文献与运行时 API 为准 [1]。
位置透明:本地引用与远程引用在发送原语上尽量统一(实现程度因框架而异),便于扩展到集群;这与「天然分片边界」一起支撑大规模伸缩。
Actor 模型要解决的核心问题
传统共享内存多线程模型在复杂系统中常面临以下困难,Actor 从模型层面给出不同答案:
| 问题 | 传统痛点(简述) | Actor 侧的典型对策 |
|---|---|---|
| 共享状态与锁 | 锁带来性能、死锁与竞态 | 不共享可变状态;以邮箱顺序处理缩小一致性范围 |
| 封装在多线程下的失效 | 多线程可重入对象方法,破坏不变量 | 单 Actor 内串行处理消息,状态只被自己的处理逻辑修改 |
| 容错与级联失败 | 链式错误易拖垮整条链路 | 监督(Supervision):父 Actor 监视子 Actor,失败时可重启/停止/升级策略(OTP 典型) |
| 分布式扩展 | 单机心智难映射到网络 | 位置透明 + 消息作为天然跨节点原语 |
上表逐行的展开要点:(1) 锁与共享内存对比见 [1];(2) 封装破坏由「消息路径内单写者」化解;(3) 监督在 OTP/Akka/CAF 等中最成体系——裸 channel 手写须自补监督与故障策略,见下文 Go、Erlang/OTP;(4) 共识与跨域强一致须在 Actor 之上另加协议([1]–[6] 不展开共识算法)。
仅「手写
goroutine+channel」而无监督树时,容错需在应用层自补。
理论优势:可扩展性、容错与无界非确定性
可扩展性(Scalability)
Actor 之间高度解耦且 Shared-nothing,因而易在多核单机或分布式集群上水平扩展:增加核数或节点时,常见工程回报包括:
- 细粒度隔离,削弱全局锁热点 — 状态分散在众多 Actor,各自串行消化邮箱,避免「少量临界区扛全世界」。
- 分片边界清晰 — 按会话、用户、房间、聚合根切分 Actor,与水平扩展、迁移、路由自然对齐。
- 过载可观测 — 邮箱积压直接表现为延迟与队列长度,便于限流与背压;但无界邮箱也会拖垮内存,生产环境必须设限。
容错性(Fault Tolerance)与「任其崩溃」
单 Actor 失败时,隔离状态使故障不易横向污染整进程内其它逻辑;在 Erlang/OTP 等栈中,「Let it crash」与监督树结合:由父级监督者按策略重启或升级,把瞬时失败变成可运维事件(与上节「监督」一致)。是否具备同等能力取决于具体框架。
无界非确定性(Unbounded Nondeterminism)
Actor 系统里,全局可观察顺序常由消息时序与仲裁塑造——与无界非确定性 [1] 及「无全局时钟式消息序」的工程约束一致。分布式共识、跨分片强一致等见下文 「局限与适用边界」,此处不重复展开。
主要使用场景
下列领域与「高并发、容错、分区、异步」高度重合,Actor 或类 Actor 架构出现频率高:
| 场景 | 说明 |
|---|---|
| 大规模分布式业务 | 社交、电商、物联网等:海量连接、分区伸缩、节点故障下降级或自愈。 |
| 高性能 Web 与中间件 | API 网关、长连接推送、消息中间件等;RabbitMQ 基于 Erlang/OTP,其「轻量进程 + 消息」与 Actor 家族思想相近(具体架构以官方文档为准)。 |
| 游戏与实时会话 | MMO、匹配、战斗同步:玩家/NPC/房间等实体常映射为 Actor 或有邮箱的服务。 |
| 实时通信与流处理 | 聊天、推送、实时数据分析、事件流:异步消息与背压可建模清晰。 |
| 物联网 | 海量设备事件:分 Actor(或分片服务)处理,避免单点锁争用。 |
是否选用 Actor,取决于状态能否自然分片、团队能否驾驭异步与消息协议;并非所有高 QPS 系统都适合。
业界案例与产品渊源(报道与架构)
下列常出现在公开报道与溯源文章中(架构随时间演进,非当前线上拓扑结论):Facebook 聊天、Twitter 等极高并发会话分解;Apache CouchDB、Ericsson 交换系统与 Erlang 生态的渊源等。上表「中间件」一行已及 RabbitMQ,此处不赘述。
主流语言与生态(总览)
| 语言 / 平台 | 典型实现 |
|---|---|
| Erlang / Elixir | 最早将 Actor 取向的轻量进程 + 消息做成语言核心之一;Elixir 运行于 BEAM,继承 OTP |
| Scala / Java | Akka、Apache Pekko 等 |
| .NET | Orleans、Akka.NET |
| C++ | CAF、SObjectizer、嵌入式侧的 QP/C++ 等 |
| Rust | Actix 等(生态有演进,选型看版本与维护状态) |
| Go | 标准库无 Actor;channel + goroutine 或 ProtoActor / Ergo 等库 |
| Groovy | GPars 等库的 Actor API |
| Dart / Swift 等 | 社区或框架层 Actor/协程式消息模型,依平台差异大 |
| Pony | 语言级 capability + Actor 取向并发 |
| 跨运行时 | Dapr Actors 等边车风格抽象 |
下列三节按 Go → C++ → Erlang 展开常见工程套路(与上表互补)。[1] 中对 CSP 与 Actor 的关系常有专门条目;用 Go 手写「邮箱」时,也可把本节与 [2] 中术语对照一起读,避免混淆「通道」与「具名 Actor 地址」。
Go:channel + goroutine 与社区框架
语言自带的 goroutine 与 channel 为实现 Actor 提供了常见基底:一个 goroutine 跑 Actor 主循环,不断从某个 chan 读取入站消息(即 Mailbox),循环内顺序处理。Go 语言以 CSP 著称,与 Actor 强调 「地址 + 邮箱」 的组织方式不尽相同,但工程上常互相模拟;理论异同仍以 [1] 为准,入门 FAQ 类补充见 [2]。
代表性库与方向(不全):
| 项目 / 方向 | 说明 |
|---|---|
| ProtoActor | 跨语言(Go、C#、Java 等),强调分布式场景下的性能与工程化;与「手写 channel Actor」相比多了监督、路由等整层抽象。 |
| Ergo | 受 Erlang/OTP 启发:网络透明、监督树、集群成员 等,目标是在 Go 中复现类似 OTP 的容错与运维模型。 |
| Hollywood | 面向低延迟(如游戏服、交易引擎)的轻量 Actor 引擎;公开材料中曾有极高吞吐的宣称(如秒级千万级消息量级),须以官方仓库、版本与自测环境为准,不宜直接当作普适 SLA。 |
| go-actor 等 | 社区存在多个同名/近名轻量库,用 chan + goroutine 封装统一接口,减少样板代码并显式避免业务锁;选型前核对模块路径与维护状态。 |
Go 的优势是调度与工具链成熟;风险是 channel 泄漏、无界缓冲、select 死锁 与 弱类型消息 的可维护性,往往需要 protobuf / 代码生成 / 清晰协议 兜底。腾讯 trpc-go(RPC 底座 + keep-order)、bingo/stateful(一等 Actor 运行时)、tsf4g-go(实体路由 + 会话 + 消息类型级保序)与 Actor 的关系,分别见专节 「trpc-go…」「bingo…」「tsf4g-go…」;三者并列选型见 「trpc-go、bingo、tsf4g-go 实现 Actor 模型方案的横向对比」。
C++:库实现与领域分叉
标准库无内置 Actor,但高性能与可控内存使其适合自研或采用专用框架。实现上常组合 std::thread、std::async / std::future、无锁或有界队列、以及 Boost.Asio 等网络与调度组件,构建「每 Actor 一条消费路径 + 队列邮箱」。
工程上常见三条线:
- 通用服务端 / 科研 / 监控: CAF(C++ Actor Framework) —— typed actor、模式匹配、网络透明、分布式与监督;公开资料中可见用于分子仿真、网络监控、MMO 后端等方向的讨论。
- Actor + 发布订阅 + CSP 的混合: SObjectizer 在一套 API 下集成多种并发范式,不少团队用于已在生产验证过的业务线延续。
- 嵌入式 / 硬实时: QP/C++ 等主动对象(Active Object)模型与 Actor 思想同构(单队列、顺序消化事件),面向微控制器与确定性时延。
代价仍是 RAII、内存序、模板实例化体积 与 线上调试。C++20 协程与 Actor 属于不同抽象,可混用于同一项目,此处不展开。
Erlang / OTP:语言级 Actor 与工业级套件
Erlang 常被视为 Actor 模型的先驱与最典型代表之一:思想直接融入语言内核,也是最早大规模工程化的范例——进程即 Actor,极轻量(创建与上下文切换成本低)、完全隔离、不共享内存;无需第三方库 即可获得完整语义。[5] 专讲 Erlang 与 Actor 的对应关系,适合与上文 [1] 的定义条目交叉读。
| 机制 | 含义 |
|---|---|
spawn |
创建新进程(新 Actor) |
! |
异步发送消息 |
receive |
从邮箱取消息并可 模式匹配 |
OTP 把 Actor 模型推到可用性峰值:监督树、gen_server 等行为模式、「任其崩溃」哲学下的重启策略,使电信运营商级耐久度成为可复制的架构模式,而非仅靠程序员自觉。
Erlang/OTP 与「死锁」:防什么、不防什么
严格说,Erlang/OTP 并没有通用的「运行时死锁检测 + 自动拆环」:除了像 gen_server:call(self(), …) 这类调用自己会被直接拦下(如 calling_self)之外,不会自动识别 A → B → A 这类多进程同步调用环并替你拆开。
更准确的说法是:
- 它从模型上消掉了大多数共享内存式的「锁死锁」;
- 对仍可能出现的协议级循环等待(双向同步
call),主要靠timeout、进程失败感知与设计约束兜底; - 不是靠「等待图分析 + 自动回滚其中一个调用」。
它真正削弱的是哪一类死锁
Erlang 式 Actor 天然规避的是经典 mutex / 条件变量 / 锁顺序 问题:
- 进程不共享可变状态;
- 业务层通常没有「抢同一把业务锁」的模型;
- 状态只由拥有者进程在本地串行修改;
- 普通
!异步发送不要求发送方等接收方处理完。
若 A、B 只互相发异步消息,一般不会形成「互等锁」,只会各自把消息放进对方邮箱(仍可能有无限增长邮箱、逻辑饥饿等问题,但语义上不是经典死锁)。
同步 call 仍可能协议死锁
若使用 gen_server:call/3 一类同步调用,仍可出现与上文 bingo 类似的协议死锁:
- A 在处理消息时同步
call(B); - B 收到后又同步
call(A); - A 正阻塞等 B,无法处理 B 发回的同步请求;
- B 又开始等 A → 循环等待。
对此 OTP 不会做通用「等待图拆环」;默认是 等到超时,再打断调用(见下文)。
OTP 里常见的兜底手段
gen_server:call(Server, Req, Timeout)自带超时;超时后调用方以timeout失败返回(默认超时常见为 5000ms,以当前文档为准),而不是无限挂住——详见 gen_server 手册。gen_server:call(self(), …)一类 调用自己:运行时会直接报错(calling_self),属于明确拦掉的必挂场景。- 较新版本中对
call与迟到回复 的语义有演进(例如与 process alias、超时后迟到回复不再污染调用方邮箱等相关说明),以你所用 OTP 版本 文档为准;OTP 24 版 gen_server 手册 与 当前文档 可对照阅读。 - 被调进程崩溃、节点不可达时,调用方收到
noproc、shutdown、{nodedown, Node}等,而不是永远等下去。 receive … after Timeout -> … end是语言级「防永久阻塞」的通用写法。
「Erlang 式防死锁」的本质
不是「检测到 A 与 B 互等,然后自动回滚其中一个」,而是:
- 用 隔离状态 + 异步消息 把锁型死锁从根上压到极低;
- 再用 单向数据流、唯一 owner、分层调用 尽量避免同步环;
- 若仍出现环,则靠 超时与失败语义 把它变成可恢复的失败请求,而不是永久卡死。
典型设计手法(减少 A call B 且 B call A)
- 交互尽量用
cast/!,少用双向同步call; - 单一状态 owner,避免两个 gen_server 互相同步读写对方状态;
handle_call里避免直接回调成环;必要时{noreply, State}再gen_server:reply/2;- 调用关系尽量做成 DAG,避免「下游再同步回调上游」;
- 把 超时 当正常协议分支处理。
一句话:Erlang/OTP 擅长削弱 锁型死锁;对 A 同步等 B、B 再同步等 A 这类协议死锁,仍主要靠协议设计成不成环,以及 call 超时 + 明确失败语义 把环打断,而不是「运行时自动拆环算法」。
代价是:跨进程大数据依赖复制、函数式与 OTP 学习曲线。
trpc-go 中实现 Actor 模型的方法分析
本节讨论 腾讯 trpc-go(Go RPC 框架)与 Actor 风格 的契合度:它是通信与治理底座,不是 Erlang/Akka 式的「自带完整 Actor Runtime」。API 与包路径以你所用版本及仓库为准,下述为常见能力归纳。
一句话结论
trpc-go不是「自带完整 Actor Runtime」的框架,但已提供实现 Actor 风格分布式系统所需的大部分底座:异步 RPC、按 key 串行(keep-order)、本地直连、流式会话、一致性哈希路由、服务发现、过载与熔断、健康检查与优雅重启等。
更准确的说法是:
trpc-go = Actor 风格分布式系统的通信与调度底座,而不是 Actor 运行时本体。
业务侧通常仍需自行补齐:Actor 生命周期、状态持久化、监督树与崩溃恢复语义、全局注册表/目录、跨节点迁移与再平衡等。
Actor 语义与 trpc-go 能力映射
| Actor 语义 | trpc-go 常见能力 | 结论 |
|---|---|---|
| 身份与寻址 | 服务名/方法名、client.WithServiceName、发现、负载均衡 |
可表达「消息发往哪类 Actor 服务」 |
| 异步消息 | Unary RPC、client.WithSendOnly()、Stream RPC |
命令、通知、会话流 |
| 单 Actor 串行 | server.WithKeepOrderPreDecodeExtractor / WithKeepOrderPreUnmarshalExtractor 等,按 key 进入有序组 |
同 key 顺序、不同 key 并行 |
| 本地调用 | scope: local / all、进程内本地调用路径等 |
同进程「位置透明」下的零链路等价路径 |
| 分片与远程 | client.WithBalancerName("consistent_hash") + client.WithKey(actorID) |
同一 Actor ID 稳定落到同一节点 |
| 会话型 Actor | 各类 Stream RPC | 长连接、双向会话 |
| 治理 | timeout/cancel、熔断、过载、健康检查、优雅重启 | 工程治理,不是 OTP 式监督树 |
| 邮箱持久化、监督树 | 无公开 Runtime | 业务自建 |
关键实现:keep-order 与内部 Actor-like 调度
服务端 keep-order 会在传输链路上把请求按提取出的 key 投递到有序组;实现上多为每 key 一套串行消费路径(常表现为 goroutine + 队列 + 回收策略),与「邮箱按 key 消费」同向,但尚未上升为公开、通用的 Actor Runtime API(具体包路径以所用版本为准)。
五种常见落地方式
1. 按 key 串行的 Keyed Actor
将 actorID 作为 keep-order 的 key:同 ID 串行、不同 ID 并行;状态留在业务 map[actorID]*State(或封装类型)。
适用:房间、会话、玩家、订单、工作流实例等「同实体串行、异实体并行」。
s := trpc.NewServer(
server.WithKeepOrderPreUnmarshalExtractor(
func(ctx context.Context, req interface{}) (string, bool) {
r, ok := req.(*MyRequest)
if !ok {
return "", false
}
return r.ActorId, true
},
),
)
- Key 若更适合从原始包头/metadata 提取,可用
WithKeepOrderPreDecodeExtractor。 - 若内置 per-key 调度不满足需求,可通过
server.WithOrderedGroups(...)注入自定义有序组,与自有 mailbox 拼接。
2. 分布式分片 Actor
目标:同一 actorID 总是落到同一节点。常见组合:发现 + 一致性哈希 + client.WithKey(actorID);节点内再配合 keep-order 串行处理。
proxy := pb.NewMyActorClientProxy(
client.WithDiscoveryName("my_discovery"),
client.WithServiceName("trpc.myapp.actor.ActorService"),
client.WithBalancerName("consistent_hash"),
)
_, err := proxy.Handle(ctx, req, client.WithKey(req.ActorId))
语义分工:consistent_hash + WithKey 解决「去哪台机器」;keep-order 解决「到机后如何串行」。不自带自动迁移、状态复制、全局目录。
3. 本地 Actor:进程内零网络开销
若多 Actor 同进程,可通过 scope(如 local / remote / all:先本地、再远端等策略)把 RPC 退化为本地路径,减少序列化与网络开销。这是调用路径优化,不是邮箱抽象本身。
4. 事件型:SendOnly
client.WithSendOnly():只发不等,适合通知/事件/fire-and-forget。重要业务须自补 ack、幂等、重试、死信。
_, err := proxy.Notify(ctx, req, client.WithSendOnly())
5. 会话型:Stream RPC
长生命周期「会话 Actor」:Client / Server / 双向 Stream。注意流式 API 的常见约定:Send 与 Recv 可跨 goroutine,多 goroutine 并发 Send 通常不安全——若以 stream 当 mailbox,宜 单写者 或自行同步(以官方文档为准)。
补充:客户端 KeepOrderClient
另有一套客户端侧顺序能力(如 KeepOrderClient + multiplexed、每 host 单连接等),偏向「单逻辑发送方对链路的写出顺序」,与「服务端按 key 邮箱式消费」不同层;实体级顺序仍优先 服务端 keep-order 或自建 mailbox。
trpc-go 未直接提供的 Actor 能力
典型缺口包括:公开 Actor 生命周期 API、邮箱抽象、监督树、崩溃后自动重启子 Actor、持久化邮箱、死信队列、内建定时器、全局 Directory、跨节点迁移与状态托管等。
工程边界:
- 默认仍是请求驱动的并发 RPC;未开 keep-order 或未自建 mailbox 时,同一实体上的请求未必有序。
- 若在 RPC handler 内起异步 goroutine,勿长期误用入口
ctx;宜用框架提供的异步工具(如trpc.Go)或克隆上下文。
实践建议(摘要)
- 以
actorID为业务主键。 - 客户端
consistent_hash+WithKey(actorID)做单 Actor 所在节点。 - 服务端
WithKeepOrderPreUnmarshalExtractor/PreDecode做同机串行。 - 状态由业务维护(
map或显式 Actor 封装)。 - 通知类走 SendOnly;长会话走 Stream;同进程优先 scope 本地路径。
- 熔断、过载、健康检查、优雅重启当作治理手段,不等价于监督树。
判断:若需求是「同一实体按顺序处理消息」,trpc-go 通常够用;若目标是 Erlang/Akka 级完整 Runtime,trpc-go 只能作传输与部署层,上层仍需独立运行时或用 keep-order + 业务封装渐进演进。
延伸阅读:trpc-go 为开源项目,keep-order、本地调用、流式、一致性哈希等可在 官方仓库 按模块浏览;示例见 examples/features/ 下 keep-order、scope、sendonly、stream 等(路径随版本可能调整)。
与上一节 trpc-go(RPC 底座 + keep-order)相对,下一节 bingo 的 stateful 运行时更接近「一等 Actor Runtime」路径——二者定位不同,可对照阅读。
bingo 中实现 Actor 模型的方法分析
本节讨论 bingo 框架(Go,常见于腾讯系有状态游戏/业务服)中的 stateful 运行时与 Actor 模型 的关系。包名、路径与选项以你所用仓库版本为准;下述基于当前常见实现归纳。
一句话结论
若说 trpc-go 是「拼出 Actor 风格系统的通信底座」,则 bingo 的 stateful 更接近 一等 Actor 运行时:在运行时内直接提供 Actor 身份与寻址、自动/手动创建、每 Actor 单 goroutine 串行、邮箱投递、生命周期回调、自动回包与延迟通知、自动存盘、Tick、广播、优雅退出、Actor 迁移 等。
更准确的判断是:
bingo/stateful ≈ 带工程治理能力的业务 Actor 运行时,而非「仅靠 RPC + 哈希自拼」。
仍未完全覆盖的典型能力包括:父子监督树、持久化邮箱、通用 ActorRef/Ask、跨集群统一目录、通用崩溃重启策略编排(与 Erlang/OTP 级平台仍有距离)。
Actor 语义与 bingo 能力映射
| Actor 语义 | bingo 常见能力 | 结论 |
|---|---|---|
| 身份 | 协议头中的目标 Actor 标识、代码生成选项(如 uid 绑定) | 身份在协议/生成链路中内建 |
| 寻址 | 路由与 handler 生成、路由头 | 消息可直接投递到目标 Actor |
| 邮箱 | 每 Actor 私有 channel / 队列 | 每 Actor 独立内存邮箱 |
| 串行 | 每 Actor 单 goroutine 主循环 | 同 Actor 天然串行,异 Actor 并发 |
| 生命周期 | OnAutoInit、OnManualInit、AfterInit、OnTick、OnGracefulExit 等 |
运行时显式托管 |
| 状态 | 业务 Actor + 存盘/变更展示等回调 | 对象即状态 的典型写法 |
| 通信 | 内建通知与同步/异步 RPC 面 | 内建通知与 RPC 面 |
| 持久化 | 周期存盘、按句柄落盘等策略 | 内建存盘策略 |
| 迁移 | 编码迁移数据、按数据重建 Actor、迁移控制器等 | 内建迁移链路 |
| 广播 | 本地 Actor 集合广播 API | 本地 Actor 集合广播,常带 QPS/队列 治理 |
| 监督 | 死循环检测、优雅退出、迁移失败恢复钩子等 | 治理与恢复,非 OTP 式监督树 |
核心结构:不是约定,而是运行时原生模型
从实现结构看,stateful 的核心事实是:
- 每个 Actor 对应一个运行时对象,自有邮箱(通常为 channel);
- Actor 管理器负责创建、登记、回收;
- 主循环驱动单 Actor 内:消息处理、Tick、退出、迁移等同一调度路径。
因此 bingo 的 Actor 是框架原生执行模型,而非「业务约定用 goroutine 模拟」。
身份与寻址(协议级)
运行时通过协议头中的目标 Actor 标识与代码生成选项(如将 uid 固化为 ActorID、服务绑定某类 Actor)确定投递目标。Actor 身份是协议与生成器协同的一等概念,寻址不必完全依赖业务自建 map 或自建 hash key(与「底座式 RPC」对比时这是强 Runtime 特征)。
邮箱、串行与消息路径
典型路径:分发层按目标 Actor 标识查找实例 → 不存在则按配置自动创建或报错 → 构造处理上下文 → 投递该 Actor 邮箱 → 主循环串行消费。语义:同 Actor 永远串行,异 Actor 并发;邮箱一般有界,满则背压/错误。
生命周期与同步阻塞式业务
业务实现初始化、Tick、优雅退出、存盘、迁移数据编码与失败恢复等生命周期回调;可配置空闲存活时间与进程级优雅关停。
重要设计选择:Actor 内允许同步阻塞写法(框架提供的通知与 RPC API),业务可像顺序程序一样写逻辑。同步 RPC 会阻塞当前 Actor goroutine,阻塞期间同 Actor 后续消息不入队处理——这是串行语义的自然结果,不是 bug。
部分文档曾描述「请求与响应走不同 channel」;当前常见实现里,同步 RPC 往往是 Actor goroutine 在 RPC 宿主上阻塞等待,响应经框架唤醒路径(如 RPC 宿主上的 activeCalls、每调用私有唤醒通道)返回,未必再排进 Actor 邮箱——RPC 占用该 Actor 的串行时间片更「显性」。
同步互调与死锁:bingo/stateful 如何处理
bingo/stateful 没有内建「自动打破 Actor 间循环等待」的机制。 对 Actor A → 同步访问 Actor B → B 再同步访问 A 这类路径,策略本质上是:允许同步阻塞、不主动检测调用环、用 RPC 超时 / deadline 让等待最终以错误返回、再用 deadLoopChecker 做「卡死 / 长时间无心跳」的观测与告警。更准确地说:它是 「允许阻塞 + 用超时与检测兜底」——语言级 Erlang/OTP 同样不做「通用运行时拆环」(见上文 「Erlang/OTP」 小节),区别主要在 隔离与消息原语由语言保证,以及 gen_server:call 超时、calling_self 拦截 等具体机制。
为什么会出现循环等待
stateful 下每个 Actor 仍是 单 goroutine + 邮箱 + 主循环:消息在同一线程路径上顺序处理;同步 RPC 会在当前 Actor 的 goroutine里阻塞等待对端返回(由 RPC 宿主里的 select/定时器等实现)。
于是可出现经典 A↔B 同步互调:
- A 正在处理某条消息,内部发起同步 RPC 调 B。
- A 的 goroutine 阻塞,无法继续消费自己的邮箱。
- B 收到请求并开始处理;若 B 再同步 RPC 回 A。
- 发往 A 的请求虽可进入 A 的邮箱,但 A 仍卡在第 2 步,无法从邮箱里取出这条消息。
- B 等 A,A 等 B,形成 循环等待——这是 串行 Actor + 同步 RPC 组合下的典型风险,不是「邮箱模型坏了」,而是 调用拓扑与 阻塞语义共同导致。
框架侧的三类兜底(都不是主动拆环)
1. RPC 超时 / deadline
- RPC 宿主对单次调用不会无限等待:在收到响应与定时器超时之间
select;超时返回RPCTimeout(或等价错误码,以 proto/版本为准)。 - 若上下文上已有 deadline,子 RPC 通常会继承剩余时间;实现里还常见对嵌套 RPC 的额外时间预留(如量级 毫秒级),避免层层把剩余时间吃光。
含义:A/B 不会「永远挂死」在无超时配置的理想世界里,但会卡到超时才以错误打断;业务须把超时当可预期分支,而不是偶发噪声。
2. deadLoopChecker(死循环 / 卡死探测)
- 周期性检查 Actor 是否过久未上报心跳;超过配置(如
GrHBTimeoutSec一类)会判定为 may in deadloop 等,并打日志、可触发注册的死循环回调。 - 它做的是 「发现问题」:不会自动取消当前 RPC、不会抢占
runLoop、不会替你恢复业务状态。
3. 超时后的「迟到响应」
- 超时后,RPC 宿主会清理当次
activeCalls记录;若对端响应晚于超时到达,HandleRPCResp一类路径可能找不到对应调用,响应被视为迟到废包,不再唤醒已返回超时的调用方。
业务上:超时返回后,同一逻辑链上不要再假设对端还会「正常回包」,需按幂等 / 补偿 设计。
与文档表述的差异(易误解点)
部分 README 用「请求走 Task、响应走 RPC Chan」一类说法帮助理解;当前实现里,响应常经 RPC 宿主 + 每调用私有通道 直接唤醒阻塞方,不一定再进一套与邮箱并行的「RPC 专用 channel」。这不改变结论:仍是 Actor goroutine 在原地阻塞等 RPC,循环互调仍会死锁,只是唤醒实现细节与旧文档插图可能不一致——以所读版本源码为准。
是否内建「异步 RPC」来绕开互调
在常见 stateful 发送路径里,若文档/代码标明 异步 RPC 到对端在 stateful 侧 不支持 或强约束,则不能指望框架把 A↔B 同步互调自动改成安全异步;需要业务改拓扑,或把部分逻辑放到 async / stateless 等模型上(见上表)。
实战建议(摘要)
- 不要依赖 A↔B 双向同步 RPC 完成闭环;可 A 同步调 B,但 B 在同一条调用链上再同步调回 A 极易触发上述环。
- 把状态裁决权收敛到单一 owner Actor;其他 Actor 发通知 / 单向请求,避免反向同步等待。
- 能用
Ntf*/ 延迟通知 的路径,少用同步RPCServer。 - 必须跨对象多跳协作时,拆到
async/stateless,或拆消息阶段,避免两个statefulActor 互相卡住。 RPCTimeOut/ RPC 超时时长 与GrHBTimeoutSec(或等价心跳阈值)设得偏保守,让问题尽快暴露。- 在业务上把
RPCTimeout、deadline exceeded当成要补偿 / 重试 / 回滚的一等错误,与普通失败区分。
一句话:在 bingo/stateful 里,A 同步调 B、B 再同步调 A 会形成真实循环等待;框架不会自动化解,主要靠 超时错误 打断,并用 deadLoopChecker 把「长时间卡住」暴露出来。
自动回包、存盘与过滤器
框架在请求处理链中常内建:handler 调用、过滤器链、请求自动回包、无回包时的占位逻辑、延迟动作、变更挂尾包、按配置触发存盘等——属于「Actor 开发工作流」而不仅是排队执行。
自动创建与手动创建
- 自动:消息到达且 Actor 不存在时按配置自动创建;关闭则返回约定错误码。
- 手动:显式 创建 Actor API。
- 可配置 Actor 数量上限。适合「登录即建」与「按需加载」两类模式。
Tick、空闲退出与死循环检测
周期 Tick 与空闲存活时间驱动回收;死循环/卡死检测(如 deadLoopChecker)与心跳超时等做探测——在 同步互调 场景下主要是观测与告警,不等价自动解除死锁(详见上文 「同步互调与死锁」)。Actor 可被动收消息,也可主动被 Tick 驱动。
迁移
迁移通知、数据编码、按数据重建实例、优雅迁移与失败恢复等构成旧新服协同链路——这是 bingo 相对「仅本地 Actor」框架的显著工程能力;业务仍须保证迁移数据与副作用一致性。
广播
支持向本地所有 Actor 投递通知,常配合 QPS、队列长度、分片节流 等治理,适合公告、赛季、全服事件等。
stateful / async / stateless
| 运行时 | 角色 |
|---|---|
stateful |
标准 Actor 语义:每 Actor 单 goroutine、串行、持状态、完整生命周期——本文「bingo Actor」默认指它。 |
async |
单线程异步回调/事件循环取向,不适合在同一路径上阻塞式长 RPC;用于不能被单 Actor 串行拖死的延迟敏感逻辑,与 stateful 互补。 |
stateless |
每请求独立 goroutine、无 Actor 长状态,偏纯 RPC 服务,不是 Actor 模型。 |
三种落地方式(摘要)
- 经典实体 Actor:一玩家/一房间/一公会/一订单一个 stateful Actor——强顺序、强状态(bingo 最擅长)。
- Main stateful + async 协作:主状态在 stateful,高频非阻塞段放 async,消息或 channel 协同(README 常见建议)。
- 可迁移 Actor:实现迁移数据编码、优雅迁移与失败恢复等接口,服务缩扩容与实例切换。
bingo 仍未直接提供的典型能力
包括但不限于:父子层级、Supervisor Tree、restart/resume/escalate 编排、持久化邮箱、死信邮箱、通用 ActorRef、任意节点透明寻址的统一目录、恰好一次投递语义等。
工程边界(务必在读源码时核实版本):邮箱多为内存有界队列;stateful 内同步 RPC 长时间阻塞会拖住同 Actor;双向同步 RPC 可能形成 Actor 间循环等待,框架侧以超时与死循环检测兜底而非自动拆环(详见上文 「同步互调与死锁」);官方文档常对同类服务之间的同步调用方式另有约束,选型时须对照最新说明;迁移再强也需业务保证数据与副作用正确。
与 trpc-go 的对照判断
- trpc-go:底座——consistent_hash + WithKey + keep-order 拼出 Keyed/Sharded Actor 语义,生命周期与邮箱多靠业务。
- bingo/stateful:运行时——协议级 Actor 身份、每 Actor 邮箱 + 主循环、生命周期与存盘/迁移/广播一体化。
若需求是单实体强顺序 + 生命周期 + 自动存盘/回包 + 平滑迁移,bingo/stateful 往往更贴合;若只需「同 key 保序 RPC」且栈已统一在 tRPC,trpc-go 更轻。若目标是完整监督树 + 持久邮箱 + 通用 Actor 平台,二者都需上层补齐。tsf4g-go(实体路由 + 会话 + 可选保序)的定位见下节。
延伸阅读:bingo 若为你所用仓库中的内部或受限发行框架,请以随版本提供的 README、示例与 proto为准,勿在本文罗列具体相对路径。
下一节 tsf4g-go 侧重实体路由、会话与消息类型级保序,不把「每实体 mailbox + run loop」框架化,与 bingo、trpc-go 形成对照。
tsf4g-go 中实现 Actor 模型的方法分析
本节讨论 tsf4g-go(腾讯系微服务/游戏服框架,常与进程间通信总线、通用 RPC 栈等配合)与 Actor 风格 的关系。具体 API 与配置以你所用版本及文档为准,下述为概念归纳,不罗列内部路径或标志位真名。
一句话结论
tsf4g-go 不是像 bingo/stateful 那样把 Actor Runtime 直接做进框架;更接近:
带会话管理、实体路由、可选保序与实例级治理的微服务框架。
它能很好承载 Actor 风格的实体寻址与消息路由(按实体 ID / 自定义信息寻址、路由注册与查询、冻结/解冻、部分消息类型串行、会话缓存等),但不直接提供:每 Actor 独立 mailbox、每 actor 独立 goroutine/run loop、框架级生命周期钩子、本地状态对象托管、监督树、持久化邮箱、与 bingo 同级的 Actor 迁移运行时等。
更准确判断:适合「Actor 风格实体路由系统」,不等于「完整 Actor 运行时」。
Actor 语义与 tsf4g-go 能力映射
| Actor 语义 | tsf4g-go 常见能力 | 结论 |
|---|---|---|
| 身份与寻址 | 按实体 ID / 自定义信息发送、实体路由子系统上的注册与查询 | 表达「把消息发给某实体」 |
| 分布式定位 | 路由表、交付注册、按 ID 查询所在实例 | 表达「实体当前在哪个实例」 |
| 异步投递 | 服务间请求、广播、多播、客户端推送 | 事件/命令通道 |
| 串行执行 | 保序配置 + 保序消息处理器 | 按消息类型/处理器串行,不是按 entityID 天然 per-Actor 串行 |
| 状态 | 会话管理组件、业务自管内存 | 框架偏 session 缓存,不托管「Actor 对象」 |
| 生命周期 | 实例级优雅停机、路由冻结/解冻等 | 服务/路由治理,不是单 Actor 生命周期 |
| 邮箱 | 无公开 mailbox 抽象 | 业务自建 |
| 监督 | 无监督树 | 业务或外部系统 |
关键发现:「有序」与「实体」是两套能力
- 实体路由子系统:实体/路由寻址(按 ID 或自定义信息发送、广播、多播、迁移中的不可用与重发等)。
- 保序配置 + 保序处理器:某类消息是否进入单 goroutine串行(服务端/转发/RPC 等通道按配置分流)。
- 会话管理组件:会话索引与缓存,不是「每 session 一个 Actor runtime」。
- 任务管理组件:异步任务、超时、重试——控制面任务,不是实体 mailbox。
组合起来可做强 Actor 风格系统,但框架未收敛为「每实体 = mailbox + run loop + 生命周期」。
MicroService 并发:先按消息类型分流
典型路径:底层 I/O 接收 → 服务实例 → 按消息类型分发到对应处理器;需保序的类型进入 保序处理器(单 channel + 单 goroutine),其余进协程池。第一层边界是服务实例,第二层是消息类型处理器——不是 entityID 级 mailbox。因此默认不是「一实体一邮箱」。
keep-order 的边界:按处理器串行,不是按实体串行
保序处理器多为 单 channel + 单 goroutine 串行执行。若配置使某几类消息共用一个 保序路径,则不同玩家/房间的同类消息也可能被同一串行通道串行化——能解决「某类消息全局保序」,但易与「同 actorID 串行、不同 actorID 并行」的 Actor 语义混淆;若误当「实体 Actor」用,可能把整类消息打成单 goroutine 瓶颈。
实体路由子系统:最接近 Actor 目录的部分
提供 路由 ID 生成、注册/查询、交付回调、按 ID 或信息发送 等能力;对外常暴露「按实体 ID / 自定义信息发请求」一类 API。语义接近 ActorRef / 实体目录 / 位置透明寻址,解决的是发到谁、发到哪台实例,不是目标实例内如何 per-entity 串行执行。
冻结/解冻与迁移治理
冻结/解冻、迁移中的路由不可用与重发等表达路由持有关系的治理,不是 bingo 式 Actor 状态 + mailbox + 生命周期 一体化迁移;业务搬迁与重放仍须自管。
发送侧保序与 RPC 接入
各类保序标志位(发送路径、转发、实体路由等)语义不一:发送侧保序不等于接收侧一定进入同一套串行模型;RPC 接入常先排队,再由下游决定是否并发——属于通道调度,≠ 标准「单 Actor 串行处理业务请求」语义。细节以版本文档为准。
会话管理与任务管理
- 会话管理:会话缓存与索引,不等价「每 session 独立 goroutine + 队列 + 生命周期回调」。
- 任务管理:一任务一 goroutine 的异步任务/重试模型,不是实体 mailbox。
优雅退出(实例级)
优雅停机、注销、摘流量回调等面向服务实例与停机窗口,不是单个 Actor 的创建/销毁语义。
三种 Actor 风格落地方式
- 实体路由 + 业务层 Keyed Mailbox(推荐):由框架把消息稳定送到持有该实体的实例;实例内按 entityID 自建 mailbox / keyed executor / state map——路由与 per-entity 串行分层最清晰。
- 消息类型级保序:用保序配置满足「某类消息全局必须保序」——明确代价是整类串行、吞吐受单 goroutine 限制。
- Session 风格服务:会话管理 + 客户端代理 + 实体路由,会话由框架缓存、实体位置由路由解决,顺序执行仍靠业务层。
tsf4g-go 未直接提供的 Actor 能力
典型缺口:ActorRef/PID、per-actor mailbox、per-actor run loop、Actor 生命周期回调体系、本地状态托管、监督树、死信/持久邮箱、框架级 Actor 状态迁移协议等。
边界:实体路由偏路由注册与查询,不是完整 Actor registry + runtime;保序粒度在处理器,不是实体邮箱。
tsf4g-go 比「纯 RPC」更接近 Actor 思想(实体寻址与路由治理强),但未把「每实体即运行时对象」框架化;若目标是 per-entity mailbox + run loop + 生命周期体验,仍须在业务层再包一层 Actor 语义。与 trpc-go、bingo 的并列选型,见下文 「trpc-go、bingo、tsf4g-go 实现 Actor 模型方案的横向对比」。
延伸阅读:tsf4g-go 若为你所用仓库中的内部或受限发行框架,请以随版本提供的 README、示例与官方文档为准,勿在公开文中罗列具体包文件路径或内部标志位名称。
trpc-go、bingo、tsf4g-go 实现 Actor 模型方案的横向对比
一句话结论
这三个框架都能承载 Actor 思想,但方式完全不同:
bingo/stateful:最接近真正的 Actor runtimetrpc-go:最适合做 Keyed Actor / Sharded Actor 的通用底座tsf4g-go:最适合做 实体路由 + 会话驱动 的 Actor 风格微服务
如果只看「开箱即用的 Actor 完整度」,结论很明确:
bingo/stateful > trpc-go / tsf4g-go
但如果细分能力,会更准确:
- 比 Actor 运行时完整度:bingo 最强
- 比 按实体 key 串行执行:trpc-go 更自然
- 比 实体寻址、位置透明和路由治理:tsf4g-go 更强
所以它们不是简单的「谁替代谁」的关系,而是三种不同层级的 Actor 实现路径。
总体定位
| 框架 | 更准确的定位 | Actor 化方式 |
|---|---|---|
trpc-go |
RPC、服务治理与调度底座 | 通过一致性哈希、keep-order、stream、本地调用等能力「拼装」Actor |
bingo/stateful |
面向业务对象的 Actor runtime | 直接把 Actor 身份、邮箱、单线程执行、生命周期、存盘、迁移做进运行时 |
tsf4g-go |
带会话能力和实体路由的微服务框架 | 通过实体路由子系统、会话管理、消息保序和服务治理承载 Actor 风格系统 |
最核心的区别是:
bingo是「框架直接替你运行 Actor」trpc-go是「框架给你足够多的底座,让你自己拼 Actor」tsf4g-go是「框架替你解决实体寻址和服务治理,但实体执行模型还要你自己补」
核心能力对比
| 维度 | trpc-go |
bingo/stateful |
tsf4g-go |
|---|---|---|---|
| Actor 身份 | 业务自定义 actorID |
协议层内建目标 Actor 标识 | entityID / 自定义名 + ID 等路由语义 |
| Actor 寻址 | 服务名 + WithKey + 一致性哈希 |
协议头直接寻址 | 实体路由:注册、查询、发送 |
| 邮箱 | 无公开邮箱;keep-order 提供 per-key 串行路径 | 每 Actor 独立私有邮箱 | 无 per-entity mailbox |
| 串行执行粒度 | 同 key 串行、不同 key 并行 | 同 Actor 串行、不同 Actor 并行 | 同消息处理器串行,不是同实体 mailbox 级串行 |
| 生命周期 | 无 Actor 生命周期 API | 自动/手动创建、Tick、空闲退出、优雅退出 | 服务实例生命周期、路由冻结/解冻 |
| 状态模型 | 业务自己维护 map[actorID]*State |
Actor 对象即状态对象 | 会话由框架缓存,实体状态仍靠业务维护 |
| 持久化 | 无内建 | 存盘、变化同步等内建策略 | 无 Actor 状态持久化模型 |
| 迁移 | 无内建 Actor 迁移 | 内建 Actor 迁移与恢复 | 有路由迁移治理,无 Actor runtime 级一体化迁移 |
| 容错治理 | 熔断、过载、健康检查、优雅重启 | 优雅退出、死循环检测、迁移失败恢复等 | 优雅退出、任务超时重试、路由治理 |
| 最适合的模型 | Keyed Actor、Sharded Actor、Session Actor | 玩家/房间/订单等强状态业务 Actor | 实体路由型逻辑服、接入态强的游戏微服务 |
三个最重要的差异
1. 谁负责 Actor 身份与寻址
bingo 的 Actor 身份最「第一性」。
- Actor 标识在协议层就是一等字段
- 分发与运行时围绕该标识工作
- 代码生成、消息头、运行时是联动的
trpc-go 的 Actor 身份最「业务约定化」。
- 框架不内建统一 ActorID 类型
- 需要业务把某个字段定义为
actorID - 再用
WithKey(actorID)和 keep-order 把它变成 Actor 风格执行单元
tsf4g-go 的身份体系最像「实体目录」。
entityID、自定义名 + ID 等已是框架显式概念- 实体路由子系统负责把实体定位到实例
- 很像 ActorRef 背后的目录服务
结论:
- 想要「身份就是协议的一部分」,选
bingo - 想要「业务自己定义 key,再映射成 Actor」,选
trpc-go - 想要「先解决实体地址和路由,再做实体执行模型」,选
tsf4g-go
2. 谁负责串行执行
这其实是三者差异最大的地方。
bingo 的串行执行是标准 Actor 语义:
- 每个 Actor 一个邮箱
- 每个 Actor 一个 goroutine / 主循环
- 同一 Actor 永远串行
- 不同 Actor 天然并行
trpc-go 的串行执行是「按 key 保序」:
- 通过 keep-order extractor 提取 key
- 同一个 key 串行
- 不同 key 并行
- 这已经非常接近 Keyed Actor
tsf4g-go 的串行执行是「按消息处理器保序」:
- 某类消息进入保序处理器
- 该处理器内部单 goroutine 串行
- 不是按
entityID拆 mailbox
这意味着:
bingo解决的是「Actor 如何执行」trpc-go解决的是「如何把实体 key 映射成串行执行单元」tsf4g-go更多解决的是「某类消息是否需要顺序消费」
结论:
- 若最关心「同一个实体天然串行」,
bingo和trpc-go都更合适 - 若只关心「这类消息要保序」,
tsf4g-go足够 - 不应把
tsf4g-go的保序配置直接等同于 per-entity Actor mailbox
3. 谁负责状态、生命周期和迁移
bingo 在这方面最完整。它直接提供:自动/手动创建、生命周期钩子、Tick、空闲退出、自动存盘、变化同步、Actor 迁移与恢复等——不只是「消息怎么排队」,而是「对象怎么生、怎么活、怎么迁、怎么存」。
trpc-go 在这方面最轻:主要提供通信、调度、路由与工程治理;状态对象、生命周期、回收、持久化、迁移多须业务自建。
tsf4g-go 则处于中间层:提供实体路由治理、冻结/解冻、会话缓存、服务实例优雅退出等;不提供 Actor 级生命周期、Actor 级持久化、Actor 级执行循环。
结论:
- 想直接写「有生命的业务对象」,选
bingo - 想完全自定义 Actor runtime,只借力 RPC 和治理,选
trpc-go - 想做「有实体路由和迁移窗口的微服务」,选
tsf4g-go
三个框架各自最像 Actor 的那部分
trpc-go
最像 Actor 的部分是:
consistent_hash+WithKey(actorID)WithKeepOrderPreDecodeExtractor/WithKeepOrderPreUnmarshalExtractor- keep-order 背后的「同 key 串行、异 key 并行」语义(实现见 官方仓库)
它像的是:Keyed Actor、Sharded Actor、同 key 串行。
bingo/stateful
最像 Actor 的部分是:
- 运行时对象、每 Actor 邮箱、主循环、Actor 管理器
- Tick、存盘、迁移数据编码 等一体化路径
它像的是:运行时原生 Actor、「对象即状态」、「邮箱即执行边界」。
tsf4g-go
最像 Actor 的部分是:
- 实体路由子系统(注册、查询、按 ID/信息发送)
- entityID / 自定义路由键、冻结/解冻、路由发送与广播/多播
它像的是:Actor 目录、实体寻址、位置透明路由。
选型建议
1. 如果你要「直接写 Actor 业务对象」
优先选 bingo/stateful。核心能力(身份、邮箱、串行、生命周期、状态持有、迁移)多已内建。典型场景:玩家 / 房间 / 公会 / 订单或工作流实例 Actor。
2. 如果你要「在通用微服务体系里实现 Actor」
优先选 trpc-go。适合一致性哈希分片、按 actorID 路由与串行、本地/远端透明调用。典型形态:客户端 consistent_hash + WithKey(actorID),服务端 keep-order extractor,业务层维护状态 map 或自建 mailbox。
3. 如果你要「做以实体路由为中心的游戏微服务」
优先选 tsf4g-go。在实体到实例定位、路由注册与查询、路由冻结与迁移窗口、会话缓存与客户端链路上较省力。但要记住:它解决的是「实体在哪里」,不等于已解决「实体在本地如何作为 Actor 执行」。
4. 如果你要的是「最完整的 Actor 体验」
优先级建议:
bingo/statefultrpc-go+ 业务自建 mailbox/runtimetsf4g-go+ 实体路由 + 业务自建实体执行层
这里不是说 tsf4g-go 比 trpc-go 弱,而是说:
- 若从「Actor 执行模型」出发,
trpc-go的 per-key 串行更容易直接落成 Actor - 若从「实体路由与在线迁移治理」出发,
tsf4g-go更强
最终判断
如果用一句话概括三者差异,可以这样说:
bingo:直接给你一套 Actor runtimetrpc-go:给你实现 Actor runtime 的通用基础设施tsf4g-go:给你实现实体路由型 Actor 系统的微服务框架
因此:
- 想少拼装、直接写 Actor,选
bingo - 想最大化通用性和可控性,选
trpc-go - 想优先解决实体路由、接入链路和服务治理,选
tsf4g-go
真正落地时,一个很实用的理解方式是:
bingo解决的是 Actor 自身trpc-go解决的是 Actor 的通信与调度tsf4g-go解决的是 Actor 风格实体系统的路由与治理
主要参考文档
- 本文前述
# trpc-go 中实现 Actor 模型的方法分析、# bingo 中实现 Actor 模型的方法分析、# tsf4g-go 中实现 Actor 模型的方法分析三节专节(与本节表格、结论一致时以专节细节为准)。
主要参考源码
trpc-go:开源,见 trpc-group/trpc-go,重点浏览 keep-order、一致性哈希、server/client options、流式与示例目录。bingo/tsf4g-go:以你所用仓库版本为准,参见随仓 README、示例与官方文档;不单列内部相对路径。
上文 trpc-go / bingo / tsf4g-go 三节专节与本节横向对比已把「底座与选型」说清;下面不绑定具体框架,只从游戏领域抽象地谈 Actor 的典型建模——与后文「对象路由」一节同样保持实现无关。
游戏领域的具体应用
Actor 与 会话隔离、并行战斗、分区世界 需求高度契合。[4] 从 引擎/组件化 角度讨论 Actor 与消息总线,可与下文「实体建模」对照。[6] 从中文语境梳理 Actor 与多线程、共享内存等范式的关系(与 [1][2] 交叉印证)——落到游戏服务端,可概括为:状态按游戏对象边界切分、协作靠异步消息、单对象内顺序处理,从而减少「全图一把锁」式的心智负担;下文场景均在这一脉络下展开。
实体与场景建模
- 玩家:每个在线角色一个逻辑 Actor(或一个「有邮箱的服务」),串行处理移动、技能、聊天等入站消息。
- NPC / 怪物:独立 AI 状态机,用消息驱动行为切换。
- 房间 / 副本 / 地图区域:区域 Actor 负责辖区内广播、视野同步与准入策略。
大厅、匹配与会话生命周期
- 匹配 / 房间创建:匹配结果、建房、进房可建模为对「房间 Actor」或「会话协调 Actor」的异步消息,避免在全局单例上堆锁。
- 断线重连 / 踢人 / 准备状态:会话内事件按顺序进入该房间或玩家的邮箱,与 [6] 强调的「单 Actor 内顺序语义」一致,便于推理先后与副作用。
- 跨进程 / 跨节点:逻辑上仍是「发给某 Actor 地址」;物理上则落到对象路由与分片(下一节),模型层不必混写传输细节。
社交、经济与异步通知
- 聊天 / 频道 / 公会:频道或公会可对应长期存活的 Actor(或分片后的 Actor 组),入站发言、权限变更、成员进出以消息驱动,状态局域在各自邮箱路径内。
- 邮件、交易行、拍卖、赠送:典型「发件人 / 收件箱 / 订单」结构适合消息投递 + 幂等与重试;与 Actor「邮箱」隐喻一致,也便于把非关键路径与核心战斗逻辑隔离。
- 排行榜与全局计数:若用单 Actor 扛全服排行,易成瓶颈;常拆为分片聚合、定时合并或专用存储,与上文「局限与适用边界」一致——[6] 中关于「何时不该指望一把 Actor 解决一切」的讨论同样适用。
异步与高并发战斗
技能结算、伤害、Buff 等可建模为 Actor 间消息:例如 A 向 B 投递「伤害」消息,由 B 的邮箱顺序更新血量与触发被动,避免用全局锁串行整幅地图(实际项目会在精度与确定性之间做帧同步/状态同步等取舍)。
视野与状态同步
「谁看见谁」可表达为兴趣管理:相关玩家 Actor 之间投递位置/属性更新,减少无脑全服广播;具体算法与引擎线程模型强相关,不必与 strict Actor 一一对应。
活动、定时与全服事件
- 赛季、限时活动、全服 Buff:可抽象为「活动 Actor」或「日程 Actor」驱动 Tick、阶段切换,向玩家/房间投递开始/结束类消息;与「世界 Boss 刷新」等共享事件通知语义。
- 与 [6] 的对照:中文材料里常强调 Actor 无共享可变状态、靠消息协作——活动侧即避免多线程直接改同一全局活动结构,而把变更收敛为有序消息或单 Actor 串行处理。
工业级与公开案例(仅供参考)
| 案例 | 说明 |
|---|---|
| Skynet | C + Lua 的轻量游戏服务端框架,大量服务 + 消息派发,国内使用率很高;语义上常被称作类 Actor,用于支撑许多大型多人在线项目。 |
| Halo 4 | 多家技术媒体曾报道其大规模游戏服务后端采用 Actor 取向架构,并给出高 CPU 利用率与近似线性扩展等定性描述;部分转述提到逾 90% 量级的资源利用率——应以微软或项目组的原始技术演讲/白皮为准,不宜作跨项目 SLA 外推。 |
| Metaplay SDK | 商业游戏服务端方案中有产品公开其内核采用 Akka.NET 等有状态 Actor 架构,强调资源效率与低延迟(以厂商白皮书与版本说明为准)。 |
客户端常受主线程与帧管线约束,更多用 ECS / Job System;服务端更愿意为隔离与容错支付消息拷贝。实践中常把「实体」「角色」与路由/分片一并讨论——这与对象路由(下一节)易混淆,故单独辨析。
Actor 模型与「对象路由」:概念辨析
Actor 模型是并发/分布式范式;对象路由是按对象 ID 把消息送到正确实例的工程机制——二者不同,常组合使用。下表与后文展开。
概念本质不同
| Actor 模型 | 对象路由 | |
|---|---|---|
| 是什么 | 并发计算的理论模型:Actor 如何封装状态、如何用异步消息交互、如何顺序处理邮箱、如何应对位置透明等规则。侧重架构风格与编程范式。 | MMO、分布式游戏服等场景里,把客户端请求、事件或数据尽快、准确地交给持有对应游戏对象(玩家、NPC、场景逻辑等)的那条进程/线程/Actor 的机制。侧重寻址与分发。 |
| 关注点 | 如何组织并发与分布式逻辑、如何降低锁与组合复杂度。 | 消息怎样找到目标对象所在节点或句柄。 |
解决的问题与抽象层次
| Actor 模型 | 对象路由 | |
|---|---|---|
| 核心问题 | 如何更安全地编写并发/分布式程序、减少锁与死锁、如何配合监督等形成容错闭环。 | 在分布式拓扑下,如何把「某个对象 ID」对应的消息送到正确主机/实例。 |
| 抽象层次 | 计算模型、语言/库级范式。 | 分布式寻址、负载与一致性哈希、场景服划分等工程策略。 |
| 典型产出 | Actor 运行时或框架(Erlang/OTP、Akka、CAF 等)。 | 路由表、名字服务、一致性哈希环、Cluster Sharding 配置、区服/场景分配器等。 |
实现上的关系:路由可以建立在 Actor 上,但不是必须
基于 Actor 或类 Actor 栈做对象路由(常见做法)
- Akka Cluster Sharding:实体以
entityId分片,消息由 Sharding 层路由到持有该实体 Actor 的节点——对象路由由框架与位置透明共同完成。 - Skynet:服务即轻量 Actor,
skynet.send/skynet.call配合名字、句柄或哈希策略,把调用送到目标服务,本质是按地址/逻辑 ID 的路由。 - Orleans 等 Virtual Actor:Grain 由运行时映射到物理节点,外部只按 ID 发消息,同样是路由 + Actor 一体化。
不必依赖 Actor 模型的路由
- 单进程对象池 + 共享内存:
map<objectId, Object*>查表即可,无 Actor 语义。 - 网关 + 场景/副本进程:网关按玩家 ID 取模或查配置把包转到某场景进程;进程内可用传统多线程/协程,不必引入 Actor 邮箱模型。
为何容易混淆
- 位置透明正好与「按 ID 找人」同向:发送方不必知道目标在哪台机器,看起来像「路由问题被模型吃掉了」。
- 分片(Sharding)在 Akka、Orleans、ProtoActor 等里是标配,与游戏「按实体 ID 切分」高度同构,文档里常把二者一起讲。
- 单 Actor 串行邮箱使对象内逻辑好写,口语里容易把「实体 Actor」直接叫成「路由终点」。
口语「Actor 路由」多指基于 Actor 的分发——仍须区分模型与路由策略。
对照小结
| 问题 | 答案 |
|---|---|
| 同一技术 / 同一概念? | 不是——分别为并发模型 vs 寻址与分发。 |
| 能同栈使用吗? | 能:Actor / Virtual Actor / Sharding 常承载对象路由。 |
| 路由必须依赖 Actor? | 不必(哈希、网关、单进程查表等均可)。 |
设计时:Actor 作内核/中间件能力,对象路由作须单独设计的上层能力;不可划等号。
局限与适用边界
- 需要全局强一致共享概念时:如账户余额、全表计数等,若草率塞进单一 Actor 会成串行瓶颈;通常要 CRDT、显式分片 + 事务/协议层或专用存储,在设计与取舍上往往比 naive Actor 网格更麻烦。
- 全局共识与跨域事务:分布式 Actor 网络中达成 Global Consensus 或跨 Actor 强一致事务仍难,常需 Raft/Paxos、协调服务或数据库层(Actor 不替代这些)。
- 思维模型切换:从同步顺序迁到异步消息流,设计、测试与排错成本更高;对开发者心智与规范(消息协议、可观测性)要求更严。
- 邮箱无界:必须限长、丢弃策略、背压,否则 OOM。
- 调试:依赖消息 ID、因果追踪、结构化日志。
小结
隔离状态 + 异步消息 提供高于「共享内存 + 锁」的一层抽象;是否采用仍看领域与团队。
技术选型与延伸阅读:语言与框架见上文各节;trpc-go / bingo / tsf4g-go 三框架与 Actor 的对照见专节,并列选型与能力表见 「横向对比」一节;对象路由 ≠ Actor;文献以 [1] 为主,[2][5][4] 与 [3][6] 按主题选读。落地前自问:状态能否按消息边界切分、能否把异步协议与观测做规范。
参考链接
| 编号 | 主题速览 | 链接 | 文中主要对照点 |
|---|---|---|---|
| [1] | 理论总览与历史 | Actor model — Wikipedia | 权威定义、三条公理、地址/别名、无界非确定性、与 CSP/图灵机关系等形式化脉络 |
| [2] | 通俗综述(FAQ) | Everything you always wanted to know about the Actor Model… | 面向工程师的 FAQ、常见误解与术语 |
| [3] | 学位论文节选 | Diploma thesis excerpt — Actors(berb.github.io) | 理论节选、与传统并发模型的并列阅读 |
| [4] | 引擎与 Actor | ejoy/ant Wiki — ActorModel | 游戏/引擎语境下的 Actor 与消息、模块划分 |
| [5] | Erlang 与 Actor | Erlang’s Actor Model — DZone | Erlang/OTP 与 Actor、轻量进程与消息 |
| [6] | 中文补充 | 知乎专栏 — Actor 模型相关梳理 | 中文语境下 Actor 与并发范式;上文「游戏领域的具体应用」中大厅/社交/活动等落点可与 [1][2] 对照阅读 |