《游戏服务端编程实践》2.2.1 Actor 模型详解(Akka / Erlang)

解析游戏服务器的 Actor 模型,包括其原理、优势与劣势。同时,介绍异步 I/O 模型,展示如何在不阻塞线程的情况下处理多个连接。

一、引言:为什么我们需要 Actor 模型?

在上一节我们学习了多线程与协程的差异。
线程和协程虽然解决了“并发执行”的问题,但它们都无法直接解决另一个根本性问题:

状态共享与并发安全。

在传统多线程程序中,多个线程共享同一块内存:

  • 同时修改变量;
  • 争夺锁;
  • 产生死锁、竞态条件(race condition)。

协程虽然降低了上下文开销,但如果仍然共享状态,本质问题仍在。

于是,计算机科学家 Carl Hewitt 在 1973 年提出了一个革命性的模型:

Actor Model:一切都是 Actor,Actor 之间只通过消息通信,不共享状态。


二、Actor 模型的核心理念

“No shared state, only message passing.”
—— Erlang Design Principle

Actor 模型中:

  • 每个 Actor 都是一个独立的实体(类似微服务或小型进程)

  • 它拥有:

    1. 自己的状态(State);
    2. 自己的行为(Behavior);
    3. 自己的消息邮箱(Mailbox)。

Actors 之间:

  • 不直接访问对方的内存;
  • 通过 异步消息传递(Asynchronous Message Passing) 通信;
  • 每个消息都是不可变对象(Immutable Message)
  • 每个 Actor 在单线程上下文中处理自己的消息,天然线程安全

三、Actor 模型的组成要素

组件职责类比(现实)
Actor独立的执行单元,持有状态“一个人”
MessageActor 间通信的数据载体“一封信”
Mailbox消息队列,按序存储待处理消息“信箱”
Dispatcher / Scheduler调度器,决定 Actor 何时执行“邮递员 + 日程安排”
Supervisor监督者,负责错误恢复“主管”

示意图

flowchart LR
A1[Actor A] -->|Message| B1[Actor B]
B1 -->|Response| A1
A1 -->|Message| C1[Actor C]

四、Actor 的运行模型

每个 Actor 是“单线程 + 消息队列”的组合:

graph TD
A[Mailbox] -->|next message| B[Actor Behavior]
B -->|process| C[State Update]
C --> A

运行规则

  1. 每次只处理一个消息;
  2. 处理完一个消息后再取下一个;
  3. 不同 Actor 可并发执行;
  4. 同一 Actor 内部逻辑是串行的;
  5. Actor 之间只通过异步消息通信。

五、Actor 的三种操作

每个 Actor 接收到消息后,可以做三件事:

  1. 发送消息 给其他 Actor;
  2. 创建新的 Actor
  3. 改变自己的行为(行为替换)

这三个操作构成了 Actor 模型的最小闭环。

六、Actor 模型的数学基础(简述)

Actor 模型不是工程概念,而是一种并发计算理论,与 Lambda Calculus(λ演算)同级。
它将“并发”抽象为:

  • Actor = 函数 + 状态 + 信箱 + 调度器

数学表达式:

Actor(a) = <Behavior, State, Mailbox>
Behavior: Message × State → (NewState, NewMessages, NewActors)

因此:

  • 每个 Actor 是一个纯函数;
  • 每次消息处理都是幂等的;
  • 无全局状态;
  • 可推理、可分布。

七、Akka:JVM 世界的 Actor 实现

Akka 是最成熟的 Actor 框架之一(Scala/Java 皆可用)。
其架构源自 Erlang OTP,但运行在 JVM 上。

Akka 核心组件结构

graph TD
A[ActorSystem]
A --> B[Dispatcher]
B --> C[Mailbox]
C --> D[Actor]
D --> E[Behavior/Receive]
组件说明
ActorSystem管理所有 Actor 的上下文
Dispatcher调度器(基于 ForkJoinPool)
Mailbox队列结构(FIFO)
ActorRefActor 引用(地址)
PropsActor 创建参数
SupervisorStrategy监督策略

八、Akka Actor 示例(Java)

public class HelloActor extends AbstractActor {
    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(String.class, msg -> {
                System.out.println("Received: " + msg);
                getSender().tell("Hello, " + msg, getSelf());
            })
            .build();
    }

    public static void main(String[] args) {
        ActorSystem system = ActorSystem.create("demo");
        ActorRef actor = system.actorOf(Props.create(HelloActor.class), "hello");
        actor.tell("World", ActorRef.noSender());
    }
}

输出:

Received: World

要点:

  • 每个 Actor 独立;
  • 消息是不可变的;
  • 通信通过 tell()
  • ActorRef 是 Actor 的唯一通信句柄。

九、消息传递机制(Akka)

9.1 发送消息

actor.tell(message, sender);

非阻塞调用,将消息放入目标 Actor 的 Mailbox。

9.2 接收消息

  • Akka 线程池从 Mailbox 拉取消息;
  • 调用 Actor.receive()
  • 处理完毕自动取下一个。

9.3 线程安全保证

  • 每个 Actor 同一时间只由一个线程执行;
  • 状态封装内部,不需锁;
  • Actor 间完全独立。

十、Erlang:原生分布式 Actor 模型

Erlang 由 Ericsson 在 1980s 为电信系统开发,
其核心语言特性就是 Actor 模型。

Erlang 的 Actor 称为“进程(process)”,
但不是 OS 进程,而是虚拟机内轻量级进程。

示例:Erlang Actor

loop(State) ->
  receive
    {msg, From, Data} ->
      NewState = handle(Data, State),
      From ! {ok, NewState},
      loop(NewState)
  end.
  • receive 是消息接收;
  • ! 是异步发送;
  • 每个进程独立运行;
  • 系统可拥有上百万个进程。

Erlang 的核心特性

特性说明
轻量进程可百万级并发
分布式原生支持可跨节点消息通信
容错模型进程崩溃不影响系统
监督树(Supervisor Tree)自动重启失败进程
热更新可无停机升级代码

这套机制让电信系统达到了“99.9999999%(九个 9)”的可靠性。

十一、Erlang OTP 监督树模型

graph TD
A[Supervisor]
A --> B[Worker 1]
A --> C[Worker 2]
A --> D[Worker 3]

Supervisor 策略:

  • One-for-One:仅重启出错进程;
  • One-for-All:组内全部重启;
  • Rest-for-One:后续进程重启。

这就是**自愈系统(Self-healing System)**的原型。

十二、Akka 的容错与监督策略

OneForOneStrategy strategy =
  new OneForOneStrategy(
    10, Duration.create("1 minute"),
    DeciderBuilder.match(Exception.class, e -> SupervisorStrategy.restart())
  );
  • Actor 崩溃 → 父 Actor 处理;

  • 父 Actor 根据策略决定:

    • 重启;
    • 停止;
    • 忽略。

“Let it crash” 是 Erlang/Akka 的哲学核心:
不要试图捕捉所有异常,让系统自己恢复。

十三、消息传递的特性

特征说明
异步发送即返回
有序每个发送者对同一接收者的消息保持顺序
不可变防止并发修改
不丢失(可靠传递)由 Mailbox 和调度器保证
无共享内存天然线程安全

十四、Actor 模型与传统并发模型对比

对比项共享内存模型Actor 模型
通信方式读写共享变量异步消息
同步机制锁/条件变量队列/异步处理
线程安全需显式加锁天然安全
可扩展性复杂
容错性异常传递困难层级监督
状态一致性难保证独立封装

十五、Akka Actor 的生命周期

阶段回调方法
创建preStart()
运行中receive()
重启前preRestart()
重启后postRestart()
停止postStop()

十六、Actor 模型在游戏服务器中的实践

模块Actor 粒度优点
玩家(Player)每个玩家一个 Actor状态隔离
房间(Room)每个战斗房间一个 Actor并发逻辑独立
世界(World)逻辑层 Actor跨模块协调
公会(Guild)分布式 Actor独立存储与逻辑
聊天Actor 池 + 广播消息有序、安全

这正是 Skynet、Akka-Cluster、Unity Photon Server 的底层理念。

十七、Actor 调度策略

Akka 提供多种 Dispatcher:

Dispatcher 类型特点应用场景
DefaultForkJoinPoolCPU 密集型任务
Pinned每 Actor 一线程独立逻辑
Balancing多 Actor 共享池动态负载
CallingThread当前线程执行测试与同步执行

十八、Akka Cluster:分布式 Actor 系统

Akka Cluster 允许 Actor 跨节点通信。

graph LR
A[Node 1] -->|Message| B[Node 2]
B -->|Reply| A
  • Actor 有全局地址(ActorPath);
  • 消息透明传输;
  • 支持节点自动加入与离开;
  • 具备一致性与容错。

这意味着:你可以把一个游戏的“公会系统”放在一台机器上,“战斗系统”放在另一台上,Actor 间通信不需任何 RPC 框架。

十九、Actor 模型的性能特征

指标AkkaErlang
单节点 Actor 数~10⁶~10⁷
消息延迟微秒级微秒级
消息吞吐10⁶/s10⁶/s
崩溃隔离SupervisorSupervisor Tree
分布式ClusterNode RPC 原生
热更新需插件原生支持

二十、与协程模型的结合

在现代游戏服务器中,常见混合架构:

层级模型示例
网络层Reactor(事件驱动)Netty / epoll
逻辑层Actor(消息驱动)Akka / Skynet
协程层Coroutine(执行驱动)Go / Lua
调度层Scheduler(负载平衡)Akka Dispatcher

这种结构结合三种并发哲学:

  • Reactor 负责 I/O
  • Actor 负责 状态与逻辑隔离
  • 协程负责 代码表达与执行效率

二十一、Actor 模型的工程价值总结

维度传统模型Actor 模型优势
状态安全需显式锁封装内安全
扩展性难以水平扩展自然分布式
容错性依赖外部监控内建 Supervisor
开发复杂度锁与共享状态难控简化为消息流
代码表达过程式事件式
可重放性可回放消息日志
分布式部署需 RPC/一致性协议内建消息透明化

二十二、应用案例

  • Erlang/WhatsApp
    单机 2 百万并发连接,消息延迟 < 100ms。
  • Akka/Lightbend
    金融风控与游戏后端框架(Play、Lagom)。
  • Skynet(Lua)
    每个服务即 Actor,协程实现消息驱动。
  • Unity Photon / SmartFox
    游戏房间即 Actor,隔离与可扩展性强。

二十三、示例:用 Akka 实现房间服务器

public class RoomActor extends AbstractActor {
    private final Map<String, Integer> players = new HashMap<>();

    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(Join.class, msg -> {
                players.put(msg.name, 100);
                broadcast(msg.name + " joined.");
            })
            .match(Attack.class, msg -> {
                players.put(msg.target, players.get(msg.target) - msg.damage);
                broadcast(msg.from + " attacks " + msg.target);
            })
            .build();
    }

    private void broadcast(String text) {
        getContext().getSystem().actorSelection("/user/players/*")
            .tell(new Message(text), getSelf());
    }
}

特点:

  • 每个房间独立 Actor;
  • 消息异步传递;
  • 无需锁;
  • 崩溃可被 Supervisor 自动恢复。

二十四、Actor 模型的设计哲学(总结)

传统并发:共享内存 + 加锁
Actor 并发:共享消息 + 独立状态

Actor 模型通过以下四个设计哲学重新定义了“并发”:

  1. 隔离(Isolation)
    每个 Actor 有自己独立的状态;
  2. 异步(Asynchrony)
    通信永远通过异步消息;
  3. 封装(Encapsulation)
    外部无法访问内部状态;
  4. 监督(Supervision)
    崩溃不是错误,是恢复契机。

二十五、思考与实践

  1. 为什么 Actor 模型能天然避免锁?
  2. 如果一个 Actor 处理消息过慢,会发生什么?如何优化?
  3. 在游戏中,是否应让“玩家”还是“房间”成为 Actor?
  4. 如何实现跨服务器 Actor 调度?
  5. 对比 Erlang 与 Akka,哪个更适合构建分布式战斗系统?

二十六、小结

核心点说明
概念并发系统由一组互相发送消息的独立 Actor 组成
通信异步、无锁、不可变
状态内部封装、线程安全
调度每个 Actor 独立运行
容错“Let it crash” 哲学,自愈式监督树
实现Erlang 原生、Akka JVM 实现、Skynet Lua 实践
应用游戏房间、公会、聊天、任务、分布式逻辑

一句话总结:

Actor 模型让我们从“线程思维”进入“消息思维”。
线程让 CPU 忙碌,Actor 让系统有序。

当你用 Actor 构建游戏世界时,每个玩家、房间、公会都像一个小宇宙,
它们彼此独立,却又通过消息联系在一起 ——
这,就是可扩展并发系统的艺术。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页