跳到主要内容

线程相关

这篇主要介绍 ioGame 框架线程相关的。

介绍

框架为开发者提供了线程相关的定制与扩展,开发者可以根据项目需求及特点,来制定符合自身项目的业务线程编排。

在简单介绍中,我们知道了 ioGame 是由网络通信框架业务框架组成。 这也意味着在 ioGame 中,可以给这两个框架的请求处理做线程相关的设置。

开发者通常只需要关注业务框架的线程设置,网络通信框架的线程并不需要关心。 通常情况下,使用框架默认的配置就足够了,除非业务特殊。

业务框架线程

线程相关的部分有两个概念,分别是

  1. ThreadExecutor线程执行器
  2. ThreadExecutorRegion线程执行器管理域

线程执行器管理域

线程执行器管理域中持有多个线程执行器,每个线程执行器中只有一个线程,而线程执行器的主要作用有两点

  1. 消费任务。
  2. 规避并发(因为每个线程执行器是单线程的,所以可以很好的避免并发问题)。

框架内置了 3 个线程执行器管理域,分别是

  1. UserThreadExecutorRegion,用户线程执行器管理域。
  2. UserVirtualThreadExecutorRegion,用户虚拟线程执行器管理域。
  3. SimpleThreadExecutorRegion,简单的线程执行器管理域。
提示

这些线程执行器也是将来与开发者接触最多的,每个线程执行器管理域都有特定作用。

业务框架与线程执行器管理域的关系

每个业务框架会关联一个线程执行器管理域,两者之间的关系是 1:1 的。


通过业务框架获取线程执行器

public final class BarSkeleton implements AttrOptionDynamic {
...
ExecutorRegion executorRegion;
}

var executorRegion = barSkeleton.getExecutorRegion();

通过 FlowContext 获取线程执行器

  • code 2,得到执行器管理域
  • code 3,得到用户线程执行器
  • code 4,得到用户虚拟线程执行器
void testExecutorRegion() {
ExecutorRegion executorRegion = flowContext.getExecutorRegion();
Executor executor = flowContext.getExecutor();
Executor virtualExecutor = flowContext.getVirtualExecutor();
}

public class FlowContext implements SimpleContext {
...
@Override
public ExecutorRegion getExecutorRegion() {
return this.getBarSkeleton().getExecutorRegion();
}
}

UserThreadExecutorRegion

UserThreadExecutorRegion 是用户线程执行器管理域,而管理域下则会有多个线程执行器。 每个用户会与其中一个线程执行器关联绑定,以避免并发问题。

该线程执行器主要用于处理 action 请求。

注意

默认的绑定规则是通过 userId 关联,多个用户可能会共用一个线程执行器, 所以不要在此线程执行器中做耗时的阻塞任务,以免阻塞其他玩家。

线程执行器管理域的线程执行器具体数量是不大于 Runtime.getRuntime().availableProcessors() 的 2 次幂。 当 availableProcessors 的值分别为 4、8、12、16、32 时,对应的数量则是 4、8、8、16、32。

availableProcessors 值Executor[] 数组实际值。(线程执行器)
44
88
128
1616
3232

使用示例

Executor executor = flowContext.getExecutor();
executor.execute(() -> {
// your code
});

// or

flowContext.execute(() -> {
// your code
});

UserVirtualThreadExecutorRegion

UserVirtualThreadExecutorRegion 是用户虚拟线程执行器管理域,内部执行器具体数量与 UserThreadExecutorRegion 一样。

该线程执行器主要用于处理 io 相关的耗时业务,如 DB 入库...等。


使用示例

Executor virtualExecutor = flowContext.getVirtualExecutor();
virtualExecutor.execute(() -> {
// your code
});

// or

flowContext.executeVirtual(() -> {
// your code
});

SimpleThreadExecutorRegion

SimpleThreadExecutorRegion 是简单的线程执行器管理域,内部执行器具体数量与 Runtime.getRuntime().availableProcessors() 相同。

该执行器与 UserThreadExecutorRegion 类似,可通过 index 来得到对应的 ThreadExecutor 执行业务,以避免并发问题。 如果业务是计算密集型的,又不想占用 UserThreadExecutorRegion 线程资源时,可使用该执行器。


使用示例

var threadIndex = flowContext.getUserId();
ExecutorRegion executorRegion = flowContext.getExecutorRegion();

var simpleThread = executorRegion.getSimpleThreadExecutor(threadIndex)
simpleThread.execute(() -> {
// your code
});
提示

不要在此线程执行器中做耗时的阻塞任务,以免阻塞其他玩家。

线程编排

本小节主要介绍游戏逻辑服接收请求的入口与扩展, 我们可以通过 RequestMessageClientProcessorHook 接口来扩展,来做玩家与线程的关联、线程编排等逻辑。

框架为 RequestMessageClientProcessorHook 接口提供了一个默认实现, 该实现比较简单,通过 userId 取得对应的线程执行器来处理业务逻辑。 因为每个玩家使用的是其所关联的线程执行器,所以可以很好的规避并发问题。 这样,框架就解决了单个玩家的并发问题,即使玩家重新登录后,也会使用相同的线程来消费业务。

学习完本节后,你可以做一些有趣的扩展,比如通过 roomId,来获取指定线程,让同一房间内的玩家请求在一个线程中执行。

如何扩展

现在,我们想扩展一个功能,想让某些 action 在虚拟线程中处理。 在虚拟线程中处理 action,可以处理一些阻塞、耗时的请求,并且不会占用 UserThreadExecutorRegion 的资源。

为了更直观的说明,我们先看一个使用示例。 我们在 action 上添加了 VirtualThread 注解,其他代码风格保持不变,就能让 action 在虚拟线程中消费。 只要删除 VirtualThread 注解,就会恢复到默认的用户线程中处理,你的业务代码无需任何改动。

public class LoginAction {
@VirtualThread
@ActionMethod(LoginCmd.loginVerify)
public UserInfo loginVerify(LoginVerify loginVerify, MyFlowContext flowContext) {
// your DB code
...
}
}

自定义注解 我们自定义一个注解 VirtualThread。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface VirtualThread {
}

扩展 RequestMessageClientProcessorHook 接口

  • code 5~9,如果 action 添加了 VirtualThread 注解,就使用虚拟线程处理业务。
  • code 11,使用默认的策略处理 action。
public final class MyRequestMessageClientProcessorHook implements RequestMessageClientProcessorHook {
@Override
public void processLogic(BarSkeleton barSkeleton, FlowContext flowContext) {
Method actionMethod = flowContext.getActionCommand().getActionMethod();
var annotation = actionMethod.getAnnotation(VirtualThread.class);
if (Objects.nonNull(annotation)) {
flowContext.getVirtualExecutor().execute(() -> barSkeleton.handle(flowContext));
return;
}

boolean execute = ExecutorSelectKit.processLogic(barSkeleton, flowContext);
if (!execute) {
barSkeleton.handle(flowContext);
}
}
}

如何设置

创建游戏逻辑服时,将实现类设置到构建器中。

public class DemoLogicServer extends AbstractBrokerClientStartup {
...
@Override
public BrokerClientBuilder createBrokerClientBuilder() {
BrokerClientBuilder builder = BrokerClient.newBuilder();
builder.appName("DemoLogicServer");

ClientProcessorHooks hooks = new ClientProcessorHooks();
hooks.setRequestMessageClientProcessorHook(new MyRequestMessageClientProcessorHook());
builder.clientProcessorHooks(hooks);

return builder;
}
}

小结

框架解决了单个玩家的并发问题,即使玩家重新登录后,也会使用相同的线程来消费业务。 如果开发者有特殊业务的,可以通过重写 RequestMessageClientProcessorHook 接口来扩展。

在同一房间内多个玩家的并发问题上,也可以通过这种方式来扩展。 比如使用 roomId 来获取房间对应的线程执行器,让房间内的玩家使用同一个线程。 还可以通过领域事件来解决,解决方法多种多样。

在一般的系统中,解决同一房间内多个玩家的并发问题,会做成全局单线程的来消费。 甚至有的框架会引入一些开发者难以理解的概念,来解决此类并发问题。 全局单线程虽然可以很好的解决并发问题,但是无法完全的发挥和利用机器的资源。

通过该示例可以看出 ioGame 推荐的这种方式,在编程方式方面, 几乎全是过程式的写法,是符合人类阅读习惯的。 同时这种编码方式也是简单的,实际编程经验一年以上的人员在理解上基本无压力。

网络通信框架线程相关

提示

通常情况下,网络通信框架线程相关的内容使用框架默认的配置就足够了,除非业务特殊。

默认情况下,bolt 的 UserProcessor 使用的是 IO 线程池,但 bolt 也提供了用户线程池的设置。 为此,框架给 UserProcessor 提供了用户线程池设置策略。 分离 IO 线程池与业务线程池,这样服务器可以在同一时间内处理更多的请求。

框架提供了 UserProcessorExecutorStrategy 接口, 用于给 UserProcessor 构建 Executor 的策略,这样更具有灵活性与扩展性,开发者可以根据自身业务来做定制。

UserProcessor 想要触发 UserProcessorExecutorStrategy, 则需要实现 UserProcessorExecutorAware 接口。 框架在启动时,如果检测到 UserProcessor 实现了该接口, 会将 Executor 设置到 UserProcessor 中,也就是上面说的用户线程池。 通常情况下,大多数开发者是接触不到自定义 UserProcessor 业务的。

框架通过 DefaultUserProcessorExecutorStrategy 为内置的 UserProcessor 做了一些用户线程池相关的设置。 开发者可以通过重写 UserProcessorExecutorStrategy 接口,根据自身业务需求来做更好的配置。

提示

开发者可以通过中文介绍文章: 蚂蚁通信框架实践这篇文章来对 SOFABolt 做个大概的了解。 文章中介绍了,在请求接收阶段,有 IO 线程,业务线程两种线程池。

UserProcessor

UserProcessor 是 SOFABolt 提供的接口,用于处理开发者的业务。

可以重写 UserProcessor 实现类的 getExecutor 方法

  • 当 getExecutor 方法的返回值为 null ,使用 SOFABolt 的 IO 线程来处理业务,也是默认设置。
  • 当 getExecutor 方法的返回值不为 null ,则 Executor 对象是 UserProcessor 的业务线程池。
提示

see AbstractAsyncUserProcessor.java

为 UserProcessor 设置业务线程池

框架提供了 UserProcessorExecutorAware 接口, 只要 UserProcessor 实现了该接口,框架会调用 setProcessorExecutor 方法并赋值。

框架提供了 UserProcessorExecutorStrategy 接口, 用于给 UserProcessor 构建 Executor 的策略。 框架会在启动时,如果检测到 UserProcessor 实现了 UserProcessorExecutorAware 接口,就会触发一次。 通过该接口,开发者可以给 UserProcessor 配置 Executor,开发者可以根据自身业务来做定制。


设置 UserProcessorExecutorStrategy

IoGameGlobalConfig.userProcessorExecutorStrategy = new YourUserProcessorExecutorStrategy();

小结

通常情况下是,开发者是不需要扩展这部分逻辑的,除非真的满足不了业务。 所以,网络通信框架线程小节的内容了解即可。