线程编织

线程编织

适用于安全多线程模拟的异步处理框架

基础库

ThreadWeave

ThreadWeave 是一个轻量级模拟框架,允许您将开销大的逻辑从 Minecraft 线程移至其他线程,同时保持所有世界修改的线程安全性。

- 本模组不会修改 Minecraft 或修补原版代码。这是供其他模组使用的库

  • 所有世界更改仍将在主服务器线程上执行

由于线程间的竞态条件,隔离模拟无法访问实时世界更改,因此它只处理计算等操作。除非您开始制作 AI、反应堆、计算机或更复杂的系统(如物理引擎),否则这听起来可能没什么用。

对世界的执行更改(如放置方块)仍将依赖 Minecraft 线程,以避免产生竞态条件。

Minecraft 模组常受以下问题困扰:

  • 主线程上的沉重刻逻辑
  • BlockEntity 内的开销计算
  • 复杂系统(反应堆、AI、工厂)

ThreadWeave 允许您仅将计算部分移至异步执行,同时保持世界安全性完整。

模拟方法中只允许使用纯数据。


重要限制

模拟方法必须是纯方法:

  • 输入 DTO -> 输出 DTO
  • 异步执行中不允许有副作用

它是如何工作的?

ThreadWeave 通过将模组的操作分为两部分来工作:数据处理和世界更改。

Minecraft 通常所有操作都在单个线程上运行。这意味着所有计算、AI、机器和方块逻辑都在竞争有限的性能资源。

ThreadWeave 通过安全地将开销大的计算移至后台线程来改变这一点。

  1. Minecraft 刻
  2. 从 BlockEntity 提取数据
  3. 在后台线程中运行模拟
  4. 获取结果(更新后的值)
  5. 将更改应用回世界

模组开发者只需定义:

  • 使用哪些数据
  • 应该进行哪些计算
  • 如何应用结果

ThreadWeave 自动执行哪些操作?

  • 从 BlockEntity 提取数据
  • 在多个线程上运行计算
  • 防止对游戏世界的非安全访问
  • 安全地将结果应用回世界
  • 管理刻时序(正常或加速模拟)

刻模式

WORLD_SYNCED

与 Minecraft 同步运行(每秒 20 次)

用于正常游戏逻辑

ISOLATED

独立于 Minecraft 速度运行

用于可以比游戏本身运行更快或更慢的模拟

安全性

  • 从不在后台线程中访问 Minecraft 世界
  • 计算期间只使用简单数据
  • 所有世界更改都在主线程上安全进行

框架防止对以下内容的非安全访问:

  • Level
  • 实体
  • BlockEntity
  • 服务器内部结构

如何使用?

安装指南

repositories {
    maven {
        url = "https://api.modrinth.com/maven"
    }
}

dependencies {
    implementation "maven.modrinth:thread-weave:{version}"
}

别忘了在 mods.toml 中定义依赖,否则 Minecraft 会崩溃。

[[dependencies.${mod_id}]]
modId="thread_weave"
type="required"
versionRange="[{version},)"
ordering="AFTER" // 非常重要:在 ThreadWeave 之后加载你的模组
side="BOTH"

例如,你有 BlockEntity(你可以使用自定义类,无论是否是 Minecraft 的类)。

public class ReactorBlockBE extends BlockEntity implements TickableBE {
    private int heat;
    private int fuel;

    public ReactorBlockBE(BlockPos pos, BlockState state) {
        super(ThreadBlockEntities.REACTOR_BE.get(), pos, state);
    }

    @Override
    public void tick() {
        if (level == null || level.isClientSide()) return;
        heat+=20;
        fuel-=1;
    }
}

I - 你需要设置哪些变量将用于异步处理。

public record ReactorData(int heat, int fuel) implements SimulationDelta {}

II - 然后你需要拆分处理过程。只需将所有计算移到一个单独的方法中。这里你需要添加 @SimulatedThread 注解。 TickMode 可以是 Isolated 或 WorldSynced。WorldSynced 将按 Minecraft 的 TPS 运行。Isolated 将以你设置的任何刻速度运行,不依赖 Minecraft。

    //value = MOD_ID + ":reactor"
    //value = "my_mod:reactor"
    @SimulatedThread(value = MOD_ID + ":reactor", mode = TickMode.WORLD_SYNCED)
    public ReactorData simulate(ReactorData data) {
        int heat = data.heat() + 20;
        int fuel = data.fuel() - 1;
        return new ReactorData(heat, fuel);
    }

III - 最后,你需要将数据提交给框架。使用静态方法 "submit"。

    @Override
    public void tick() {
        if (level == null || level.isClientSide()) return;

        Simulations.submit(this);
        System.out.println("heat=" + this.getHeat() + " fuel=" + this.getFuel());
    }

IV - 你还需要设置哪些类将被扫描以查找 @SimulatedThread 注解。

    public YourMainModClass(IEventBus modEventBus, ModContainer modContainer) {
        modEventBus.addListener(this::commonSetup);
        Simulations.submitScan(NavMeshSimulation.class);
        ...
    }

你还可以通过 @SimulatedInterpreter 和 @SimulatedExtractor 注解手动控制解释器和提取器。

例如,工作刻:

    @SimulatedThread(value = MOD_ID + ":reactor", mode = TickMode.WORLD_SYNCED)
    public ReactorData simulate(ReactorData data) {
        int heat = data.heat() + 20;
        int fuel = data.fuel() - 1;
        return new ReactorData(heat, fuel);
    }

    @SimulatedInterpreter
    public void interpreter(NavData data) {
        this.heat = data.heat();
        this.fuel = data.fuel();
        workTicks++;
    }

上面的例子并非最佳。自定义解释器在按区域处理时更为有用,例如合并结果(烘焙导航网格生成)。

    @SimulatedThread(value = MOD_ID + ":navgraph", mode = TickMode.WORLD_SYNCED)
    public NavData simulate(NavProcessorData data) {
        if (processor.isEmpty()) return null;
        return processor.build(navLevel);
    }

    @SimulatedInterpreter
    public void interpreter(NavData data) {
        if (data == null) return;
        for (NavNode node : data.toRemove()) {
            navLevel.nodes.remove(node.id());
        }
        navLevel.nodes.putAll(data.nodes());
        navLevel.portals.putAll(data.portals());
        navLevel.chunkIndices.putAll(data.chunkIndices());
    }

这是巅峰优化吗?

不。还有很多工作要做。你可以在我的 Discord 服务器中关注进展。

例如,大多数 TickingBlockEntity 由于需要 Level(目前)而与此库配合不佳。

P.S: 我稍后会发布 Fabric 移植版和其他版本 [1.7.10 - 26.x]。