Post

Actor Model in Action

系统介绍 Actor 模型:定义与公理、Shared-nothing、理论优势、语言生态;Erlang/OTP 与死锁(同步 call、超时、非自动拆环);对比 trpc-go、bingo stateful(含同步互调与死锁)、tsf4g-go 与横向对比;游戏应用;对象路由辨析;文末六条外部参考。

Actor Model in Action

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)。不少材料用「可并行发起」来概括这三类能力——强调的是彼此的独立性,而非多线程同时改写共享内存;具体列举如下:

  1. 向已知地址发送有限条消息(通常异步、非阻塞;是否有序投递依实现)。
  2. 创建有限个新 Actor
  3. 指定接收下一条消息时的行为(即更新局部状态 / 行为,等价于顺序语义下的状态机步进,亦常称 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 手写须自补监督与故障策略,见下文 GoErlang/OTP(4) 共识与跨域强一致须在 Actor 之上另加协议([1][6] 不展开共识算法)。

仅「手写 goroutine + channel」而无监督树时,容错需在应用层自补。

理论优势:可扩展性、容错与无界非确定性

可扩展性(Scalability)

Actor 之间高度解耦Shared-nothing,因而易在多核单机或分布式集群上水平扩展:增加核数或节点时,常见工程回报包括:

  1. 细粒度隔离,削弱全局锁热点 — 状态分散在众多 Actor,各自串行消化邮箱,避免「少量临界区扛全世界」。
  2. 分片边界清晰 — 按会话、用户、房间、聚合根切分 Actor,与水平扩展、迁移、路由自然对齐。
  3. 过载可观测 — 邮箱积压直接表现为延迟与队列长度,便于限流与背压;但无界邮箱也会拖垮内存,生产环境必须设限。

容错性(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 CouchDBEricsson 交换系统与 Erlang 生态的渊源等。上表「中间件」一行已及 RabbitMQ,此处不赘述。

主流语言与生态(总览)

语言 / 平台 典型实现
Erlang / Elixir 最早将 Actor 取向的轻量进程 + 消息做成语言核心之一;Elixir 运行于 BEAM,继承 OTP
Scala / Java AkkaApache Pekko
.NET OrleansAkka.NET
C++ CAFSObjectizer、嵌入式侧的 QP/C++
Rust Actix 等(生态有演进,选型看版本与维护状态)
Go 标准库无 Actor;channel + goroutineProtoActor / 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 与社区框架

语言自带的 goroutinechannel 为实现 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::threadstd::async / std::future、无锁或有界队列、以及 Boost.Asio 等网络与调度组件,构建「每 Actor 一条消费路径 + 队列邮箱」。

工程上常见三条线:

  1. 通用服务端 / 科研 / 监控: CAF(C++ Actor Framework) —— typed actor、模式匹配、网络透明、分布式与监督;公开资料中可见用于分子仿真、网络监控、MMO 后端等方向的讨论。
  2. Actor + 发布订阅 + CSP 的混合: SObjectizer 在一套 API 下集成多种并发范式,不少团队用于已在生产验证过的业务线延续。
  3. 嵌入式 / 硬实时: 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 类似的协议死锁

  1. A 在处理消息时同步 call(B)
  2. B 收到后又同步 call(A)
  3. A 正阻塞等 B无法处理 B 发回的同步请求;
  4. 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 式防死锁」的本质

不是「检测到 AB 互等,然后自动回滚其中一个」,而是:

  • 隔离状态 + 异步消息锁型死锁从根上压到极低;
  • 再用 单向数据流、唯一 owner、分层调用 尽量避免同步环;
  • 若仍出现环,则靠 超时与失败语义 把它变成可恢复的失败请求,而不是永久卡死。

典型设计手法(减少 A call BB call A

  • 交互尽量用 cast / !,少用双向同步 call
  • 单一状态 owner,避免两个 gen_server 互相同步读写对方状态;
  • handle_call 里避免直接回调成环;必要时 {noreply, State}gen_server:reply/2
  • 调用关系尽量做成 DAG,避免「下游再同步回调上游」;
  • 超时正常协议分支处理。

一句话:Erlang/OTP 擅长削弱 锁型死锁;对 A 同步等 BB 再同步等 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 的常见约定:SendRecv 可跨 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)或克隆上下文

实践建议(摘要)

  1. actorID 为业务主键。
  2. 客户端 consistent_hash + WithKey(actorID) 做单 Actor 所在节点。
  3. 服务端 WithKeepOrderPreUnmarshalExtractor / PreDecode 做同机串行。
  4. 状态由业务维护(map 或显式 Actor 封装)。
  5. 通知类走 SendOnly;长会话走 Stream;同进程优先 scope 本地路径
  6. 熔断、过载、健康检查、优雅重启当作治理手段不等价于监督树。

判断:若需求是「同一实体按顺序处理消息」,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)相对,下一节 bingostateful 运行时更接近「一等 Actor Runtime」路径——二者定位不同,可对照阅读。

bingo 中实现 Actor 模型的方法分析

本节讨论 bingo 框架(Go,常见于腾讯系有状态游戏/业务服)中的 stateful 运行时与 Actor 模型 的关系。包名、路径与选项以你所用仓库版本为准;下述基于当前常见实现归纳。

一句话结论

若说 trpc-go 是「拼出 Actor 风格系统的通信底座」,则 bingostateful 更接近 一等 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 并发
生命周期 OnAutoInitOnManualInitAfterInitOnTickOnGracefulExit 运行时显式托管
状态 业务 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 BB 再同步访问 A 这类路径,策略本质上是:允许同步阻塞不主动检测调用环、用 RPC 超时 / deadline 让等待最终以错误返回、再用 deadLoopChecker 做「卡死 / 长时间无心跳」的观测与告警。更准确地说:它是 「允许阻塞 + 用超时与检测兜底」——语言级 Erlang/OTP 同样不做「通用运行时拆环」(见上文 「Erlang/OTP」 小节),区别主要在 隔离与消息原语由语言保证,以及 gen_server:call 超时、calling_self 拦截具体机制

为什么会出现循环等待

stateful 下每个 Actor 仍是 单 goroutine + 邮箱 + 主循环:消息在同一线程路径上顺序处理;同步 RPC 会在当前 Actor 的 goroutine阻塞等待对端返回(由 RPC 宿主里的 select/定时器等实现)。

于是可出现经典 A↔B 同步互调

  1. A 正在处理某条消息,内部发起同步 RPC 调 B
  2. A 的 goroutine 阻塞无法继续消费自己的邮箱。
  3. B 收到请求并开始处理;若 B 再同步 RPC 回 A
  4. 发往 A 的请求虽可进入 A 的邮箱,但 A 仍卡在第 2 步,无法从邮箱里取出这条消息。
  5. BAAB,形成 循环等待——这是 串行 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,或拆消息阶段,避免两个 stateful Actor 互相卡住
  • RPCTimeOut / RPC 超时时长GrHBTimeoutSec(或等价心跳阈值)设得偏保守,让问题尽快暴露
  • 在业务上把 RPCTimeoutdeadline exceeded 当成要补偿 / 重试 / 回滚一等错误,与普通失败区分。

一句话:在 bingo/stateful 里,A 同步调 BB 再同步调 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 模型。

三种落地方式(摘要)

  1. 经典实体 Actor:一玩家/一房间/一公会/一订单一个 stateful Actor——强顺序、强状态(bingo 最擅长)。
  2. Main stateful + async 协作:主状态在 stateful,高频非阻塞段放 async,消息或 channel 协同(README 常见建议)。
  3. 可迁移 Actor:实现迁移数据编码、优雅迁移与失败恢复等接口,服务缩扩容与实例切换。

bingo 仍未直接提供的典型能力

包括但不限于:父子层级Supervisor Treerestart/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」框架化,与 bingotrpc-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 风格落地方式

  1. 实体路由 + 业务层 Keyed Mailbox(推荐):由框架把消息稳定送到持有该实体的实例;实例内按 entityID 自建 mailbox / keyed executor / state map——路由per-entity 串行分层最清晰。
  2. 消息类型级保序:用保序配置满足「某类消息全局必须保序」——明确代价是整类串行、吞吐受单 goroutine 限制。
  3. Session 风格服务会话管理 + 客户端代理 + 实体路由,会话由框架缓存、实体位置由路由解决,顺序执行仍靠业务层。

tsf4g-go 未直接提供的 Actor 能力

典型缺口:ActorRef/PIDper-actor mailboxper-actor run loopActor 生命周期回调体系本地状态托管监督树死信/持久邮箱框架级 Actor 状态迁移协议等。

边界:实体路由偏路由注册与查询不是完整 Actor registry + runtime;保序粒度在处理器不是实体邮箱。

tsf4g-go 比「纯 RPC」更接近 Actor 思想(实体寻址与路由治理强),但把「每实体即运行时对象」框架化;若目标是 per-entity mailbox + run loop + 生命周期体验,仍须在业务层再包一层 Actor 语义。与 trpc-gobingo 的并列选型,见下文 「trpc-go、bingo、tsf4g-go 实现 Actor 模型方案的横向对比」

延伸阅读tsf4g-go 若为你所用仓库中的内部或受限发行框架,请以随版本提供的 README、示例与官方文档为准,勿在公开文中罗列具体包文件路径或内部标志位名称。

trpc-go、bingo、tsf4g-go 实现 Actor 模型方案的横向对比

一句话结论

这三个框架都能承载 Actor 思想,但方式完全不同:

  • bingo/stateful:最接近真正的 Actor runtime
  • trpc-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 更多解决的是「某类消息是否需要顺序消费」

结论

  • 若最关心「同一个实体天然串行」,bingotrpc-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 ActorSharded 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 体验」

优先级建议:

  1. bingo/stateful
  2. trpc-go + 业务自建 mailbox/runtime
  3. tsf4g-go + 实体路由 + 业务自建实体执行层

这里不是说 tsf4g-gotrpc-go 弱,而是说:

  • 若从「Actor 执行模型」出发,trpc-go 的 per-key 串行更容易直接落成 Actor
  • 若从「实体路由与在线迁移治理」出发,tsf4g-go 更强

最终判断

如果用一句话概括三者差异,可以这样说:

  • bingo:直接给你一套 Actor runtime
  • trpc-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:服务即轻量 Actorskynet.send / skynet.call 配合名字、句柄或哈希策略,把调用送到目标服务,本质是按地址/逻辑 ID 的路由
  • OrleansVirtual 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] 对照阅读
This post is licensed under CC BY 4.0 by the author.
Share