当前位置: 首页 > news >正文

游戏服务器使用actor模型

1.Actor 模型简介与并发优势

Actor 模型是一种基于 “并发实体(Actor)” 的分布式并发计算模型,由计算机科学家 Carl Hewitt 于 1973 年提出,旨在解决传统多线程并发编程中的锁竞争、死锁等复杂问题。其核心思想是:将系统中的所有并发单元抽象为 “Actor”,每个 Actor 是独立的计算实体,仅通过 “消息传递” 交互,不共享状态

1.1.Actor 模型的核心概念

要理解 Actor 模型,需先掌握其 3 个核心组成部分,这也是它与传统多线程模型的本质区别:

1.1.1. 核心实体:Actor

Actor 是模型的最小并发单元,可理解为 “具有状态、行为和消息邮箱的独立个体”,每个 Actor 具备 3 个关键能力:

  • 维护私有状态:Actor 的状态完全私有,仅能通过自身行为修改(外部无法直接访问或修改),从根源杜绝 “共享状态竞争”(传统多线程的核心痛点)。
  • 处理消息:Actor 通过 “接收消息” 触发行为(如计算、修改自身状态、发送新消息),消息处理是串行的(同一时间仅处理一条消息),无需加锁

1.1.2. 交互方式:异步消息传递

Actor 之间不直接调用方法,而是通过异步、无阻塞的消息传递交互,消息具有以下特性:

  • 异步性:发送方发送消息后无需等待接收方处理,可立即继续执行自身逻辑(类似现实中的 “发邮件”,无需等对方回复)。
  • 可靠性:模型通常保证 “消息按发送顺序投递”(FIFO),且不丢失(具体需依赖实现框架的保障)。
  • 无共享:消息是 Actor 间唯一的交互媒介,不存在 “共享内存”,因此无需考虑锁、信号量等同步机制。

1.1.3. 消息邮箱:Actor 的 “消息队列”

每个 Actor 都有一个消息邮箱(Mailbox),用于缓存接收的消息:

  • 外部发送的消息会先进入邮箱,Actor 按 “先进先出” 顺序依次处理(避免并发修改状态)。
  • 若 Actor 处理消息耗时较长,新消息会在邮箱中排队,不会阻塞发送方或其他 Actor。

2、传统线程性能瓶颈

2.1.线程执行“冷热不均”

    @Overridepublic void accept(BaseGameTask task) {if (task == null) {throw new NullPointerException("task is null");}if (!running.get()) {return;}int distributeKey = (int) (task.getDispatchKey() % workerPool.length);workerPool[distributeKey].receive(task);}

服务器线程模型会把客户端的IO事件封装成一个任务,这个任务根据某种hash算法进行映射,例如根据session自增长id,或者根据玩家id作hash,或者根据地图场景id作hash。无论何种策略,都无法做到每条线程一样的任务量,比如,战斗场景的任务远远高于普通场景的任务。如此,便出现了每条线程“冷热不均”。而actor作为线程的基本执行单元,不同的actor依次被线程组的线程单独执行,如此,可保证线程“近乎公平”。如图所示(绿色方块代表采样点是否处于执行状态,绿色方块均匀分布):

5000个机器人模拟玩家在线,疯狂发数据,总体而言,线程冷热均衡

2.2.业务线程分类过多

有些游戏服务器会根据功能对线程池进行分组,例如分为业务线程池,战斗线程池,聊天线程池。每一组线程池都有大量的线程,这样虽然可以隔离任务,但导致的严重后果就是线程切换额外消耗。而actor线程池不区分模块也可保证任务均匀执行,可减少线程的总数量。

3.代码实现

3.1.Actor

Actor, 是线程执行的基本单元。一个Actor代表一个对象,拥有一个邮箱,用于接收消息。

public interface Actor extends Runnable {/*** 绑定的邮箱*/Mailbox getMailBox();/*** 发送消息到当前Actor*/default void tell(Mail message) {tell(message, null);}/*** 发送消息到当前Actor** @param message 消息* @param sender  发送者*/default void tell(Mail message, Actor sender) {Objects.requireNonNull(message);message.setSender(sender);Mailbox mailBox = getMailBox();if (mailBox != null) {mailBox.receive(message);}}/*** 获取Actor模型名称,例如player, monster, guild等*/default String getModel() {return getClass().getSimpleName();}}

3.2.基础Actor

为Actor接口提供基础实现

public class AbsActor implements Actor {protected final Logger logger = LoggerFactory.getLogger(getClass());/*** 任务堆积警戒值,超过这个值会预警*/static int THRESHOLD = 100;/*** 单次处理最大任务数,防止其他任务饥饿*/static int MAX_TASKS_PER_RUN = 50;/*** 绑定的邮箱*/private Mailbox mailBox;/*** actor名称/路径*/private final String actorPath;/*** 当前任务是否在parent队列里*/private AtomicBoolean queued = new AtomicBoolean(false);
}

需要注意的是,由于java不支持多继承,继承该类后,便无法继承其他类,若需要继承其他类,建议采用组合模式,把该类作为一个属性。例如:

@Entity(name = "playerent")
@Getter
@Setter
public class PlayerEnt extends BaseEntity<String> {/*** actor邮箱*/@Setterprivate Actor actor;}

消息接收

Actor实现Runnable接口,但一开始它是不会被任意线程执行的,只有当系统往actor的邮箱里投放邮件,这个actor才开始丢到actor线程池这个一级队列,代码如下:

    @Overridepublic void tell(Mail message, Actor sender) {Objects.requireNonNull(message);if (!actorSystem.running.get()) {return;}message.setSender(sender);message.setReceiver(this);Mailbox mailBox = getMailBox();mailBox.receive(message);// 丢到线程池运行if (queued.compareAndSet(false, true)) {actorSystem.accept(this);}}

当线程池里的任务(Actor)执行时,如果actor的任务全部执行完毕,那么这个actor的任务就暂时告一段落里,线程池也不会有这个Actor(除非后续继续调用Actor#tell方法)。当Actor的任务非常多,一次性执行的话,可能导致其他任务无法按时完成任务(饥饿),会暂时终止此次Actor的执行(有点类似Thread#yield()方法)

    /*** 负责遍历和调度Mailbox中的Mail, 原子性执行,不会出现并发问题*/@Overridepublic void run() {// 防止任务一直占线int size = mailBox.getTaskSize();if (size > THRESHOLD) {logger.warn("[{}]任务堆积严重,任务数量[{}]", actorPath, size);}try {// 限制单次处理任务数量,防止饥饿int processedCount = 0;Runnable mail;while ((mail = mailBox.poll()) != null && processedCount < MAX_TASKS_PER_RUN) {mail.run();processedCount++;}} catch (Exception e) {logger.error("[{}]任务执行异常", actorPath, e);} finally {queue.set(false);// 如果还有任务,重新加入队列if (!mailBox.isEmpty()) {if (queued.compareAndSet(false, true)) {actorSystem.accept(this);}}}}

3.3.邮箱

每个 Actor 都有自己的邮箱(Mailbox),Actor 通过接收消息(Mail)来执行任务。

邮箱本质上是一个“二级队列”。邮箱根据种类可以分成多种,例如:

无界邮箱:可以存储无限的邮件

有界邮箱:可以存储有限的邮件,溢出的邮件根据策略,可能是丢弃,也可能是阻塞

优先邮箱:投递到邮箱的邮件可以设置一个优先级,高优先级的邮件会被优先执行。

 * 邮箱* 一级任务队列为线程池 {@link ActorThreadModel#threadPool}* actor模型里的邮箱, 邮箱相当于一个二级队列, 当{@link ActorThreadModel#threadPool}的每一个任务被执行时,该邮箱的任务会按顺序串行执行* 绝对不存在同一个actor的邮箱被多个线程同时执行,保证了线程安全* 需要注意的是,同一个actor的邮箱在同一时刻只会被一个线程执行,但在不同时刻,有可能在不同的线程执行*/
public class Mailbox {protected BlockingQueue<Mail> mails;public Mailbox() {this.mails = new ArrayBlockingQueue<>(512);}public Mailbox(BlockingQueue<Mail> mails) {this.mails = mails;}public void receive(Mail mail) {if (!this.mails.offer(mail)) {throw new IllegalStateException("mail box queue is full");}}/*** 获取当前邮件数量*/public int getTaskSize() {return mails.size();}public boolean isEmpty() {return mails.isEmpty();}public Mail poll() {return mails.poll();}}

3.4.邮件Mail

邮件是任务执行的基本单元,每次Actor执行的时候,会有若干个Mail被执行。

/*** 邮件抽象基类* 所有投递到Actor邮箱的消息都应该继承此类*/
public abstract class Mail extends BaseTask {/*** 邮件创建时间*/protected final long createdTime;/*** 发送者(可选,用于追踪)*/protected Actor sender;/*** 接收者(可选,用于追踪)*/protected Actor receiver;public Mail() {this.createdTime = System.currentTimeMillis();}/*** 邮件处理逻辑,子类必须实现* 负责执行具体的业务逻辑*/@Overridepublic abstract void action();}

3.5.Actor 线程池

Actor 线程池,是一个“一级任务队列”(rootQueue),可直接使用JDK提供的线程池即可。

      NamedThreadFactory threadFactory = new NamedThreadFactory("message-business");threadPool = new ThreadPoolExecutor(coreSize, coreSize, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), threadFactory);

Actor任务执行的机制如下:

rootQueue (一级队列) → Mailbox (二级队列) → Mail.run() (具体任务)

  • rootQueue: 全局任务调度,平衡负载
  • Mailbox: Actor级别的消息缓冲,避免阻塞
  • Mail.run(): 直接执行,减少上下文切换

3.6.ActorSystem

负责actor调度,创建与管理。
public class ActorThreadModel implements ThreadModel {  public Actor createActor(String actorPath) {if (actors.containsKey(actorPath)) {throw new IllegalArgumentException("Actor already exists: " + actorPath);}AbsActor actor = new AbsActor(this, actorPath, systemConfig);actors.put(actorPath, actor);logger.debug("Created actor: {}", actorPath);return actor;}public Actor getActor(String actorPath) {return actors.get(actorPath);}/*** 移除指定的actor** @param actorPath actor路径*/public void removeActor(String actorPath) {Actor removed = actors.remove(actorPath);if (removed != null) {logger.debug("Removed actor: {}", actorPath);}}/*** 获取或创建actor** @param actorPath actor路径* @return actor*/public Actor getOrCreateActor(String actorPath) {return actors.computeIfAbsent(actorPath, path -> {AbsActor actor = new AbsActor(this, path, systemConfig);logger.debug("Auto-created actor: {}", path);return actor;});}
}

4.常见问题

4.1.为什么 Mail 和 Actor 都有 run() 方法?

A: 这是两级队列架构的设计。 Actor#run() 负责遍历和调度消息,Mail.run() 负责执行具体的业务逻辑。

Actor使用Mailbox才保存任务,mailbox只代表一个二级队列,如果Actor没有run()方法, Actor线程池的任务总线,很难发挥作用。

ActorThreadPool调度分发 →不同Actor均衡投递到每条业务线程→Actor.run() → 遍历内部Mail → 调用Mail.run()

4.2.共享Actor的作用

工具库里有一个,称为SharedActor的类,如下:

public class SharedActor {/*** 由若干个Actor组成一个集体*/private final Actor[] group;public SharedActor(Actor[] group) {this.group = group;}/*** 根据key获取共享Actor** @param key* @return*/public Actor getSharedActor(long key) {int index = Math.abs((int) (key % group.length));return group[index];}/*** 获取工作线程数量*/public int getWorkerSize() {return group.length;}/*** 获取邮箱组*/public Actor[] getGroup() {return group;}}

这个对象,可以视作一种逻辑主体,例如,玩家在正式登录成功后,会将请求绑定到具体的Actor对象,但在绑定之前,这些任务,需要绑定到一个逻辑主体,并且能够保证线程安全。这就是共享Actor的作用了。

手游服务端开源框架系列完整的代码请移步github ->> jforgame


文章转载自:

http://gTWVXlZI.mdmqg.cn
http://7yXCKLvz.mdmqg.cn
http://FkuVS83j.mdmqg.cn
http://kC8M4myU.mdmqg.cn
http://3JgQ6Mb9.mdmqg.cn
http://Be7OAf5d.mdmqg.cn
http://XtFfO8hX.mdmqg.cn
http://VXFI4nfX.mdmqg.cn
http://1oiNrpVD.mdmqg.cn
http://BEalryVt.mdmqg.cn
http://8JOCTLCq.mdmqg.cn
http://QlfTPYaD.mdmqg.cn
http://jsCEktjz.mdmqg.cn
http://i253fqpj.mdmqg.cn
http://dPuQaEyg.mdmqg.cn
http://EObfNpZ2.mdmqg.cn
http://arLXRdTe.mdmqg.cn
http://vca8lxoy.mdmqg.cn
http://CRFIjC9l.mdmqg.cn
http://tYnhooYn.mdmqg.cn
http://14wIEmYs.mdmqg.cn
http://YTOUu9TX.mdmqg.cn
http://Lv73xUzG.mdmqg.cn
http://XRC6Ia42.mdmqg.cn
http://egck1GcD.mdmqg.cn
http://K1xNgYZv.mdmqg.cn
http://rLUDRqVQ.mdmqg.cn
http://lFcpaAWG.mdmqg.cn
http://hkIsJt1e.mdmqg.cn
http://GLwAu3Uq.mdmqg.cn
http://www.dtcms.com/a/381534.html

相关文章:

  • 002 Rust环境搭建
  • 2.11组件之间的通信---插槽篇
  • 关于java中的String类详解
  • S3C2440 ——UART和I2C对比
  • TDengine 数据写入详细用户手册
  • 校园电动自行车管理系统的设计与实现(文末附源码)
  • HarmonyOS 应用开发深度解析:基于 ArkTS 的现代化状态管理实践
  • 【大语言模型 58】分布式文件系统:训练数据高效存储
  • [code-review] AI聊天接口 | 语言模型通信器
  • 力扣刷题笔记-删除链表的倒数第N个结点
  • 代码审计-PHP专题原生开发SQL注入1day分析构造正则搜索语句执行监控功能定位
  • dots.llm1:小红书开源的 MoE 架构大语言模型
  • --gpu-architecture <arch> (-arch)
  • uniapp动态修改tabbar
  • Spring Boot 集成 Flowable 7.1.0 完整教程
  • 教你使用服务器如何搭建数据库
  • Kafka如何配置生产者拦截器和消费者拦截器
  • uniapp:根据目的地经纬度,名称,唤起高德/百度地图来导航,兼容App,H5,小程序
  • 欧拉函数 | 定义 / 性质 / 应用
  • 【更新至2024年】1996-2024年各省农业总产值数据(无缺失)
  • 财报季观察|消费“分野”,燕之屋(1497.HK)们向上生长
  • 机械制造专属ERP:降本增效与数字转型的关键
  • 基于node.js+vue的医院陪诊系统的设计与实现(源码+论文+部署+安装)
  • 【大语言模型 59】监控与日志系统:训练过程全面监控
  • HIS架构智能化升级编程路径:从底层原理到临床实践的深度解析(下)
  • Node.js中package.json详解
  • 当AI遇上数据库:Text2Sql.Net如何让“说人话查数据“成为现实
  • 数据结构8——双向链表
  • 问卷系统自动化测试报告
  • Python 的函数柯里化(Currying)