Skip to content
MJP
/ NMS 執行緒安全模式

核心概念

NMS 執行緒安全模式

NMS Threading Patterns

Paper NMS 開發的執行緒模型說明。所有 NMS 操作必須嚴格遵守執行緒邊界,否則會導致伺服器崩潰或數據損壞。


核心執行緒

執行緒 用途 NMS 存取
Main(MinecraftServer thread) Tick 迴圈、實體更新、世界寫入 ✅ 允許所有 NMS 存取
Netty IO threads 封包解碼/編碼 ⚠️ 僅封包讀寫,禁止存取 Level/Entity
Bukkit Async scheduler IO、DB、HTTP ❌ 禁止直接存取 NMS
Worldgen threads 世界生成 ⚠️ 僅限生成相關 API

檢查當前執行緒

import net.minecraft.server.MinecraftServer;

// 檢查是否在主執行緒
if (!MinecraftServer.getServer().isSameThread()) {
    throw new IllegalStateException("Must be called from main thread");
}

模式 1:封包攔截(Netty → Main)

封包在 Netty IO 執行緒接收,若需修改實體狀態必須切回主執行緒。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof ServerboundChatPacket chat) {
        // Netty 執行緒:只做 packet 本身的解析
        String content = chat.message();

        // 需存取實體/世界 → 切回主執行緒
        Bukkit.getScheduler().runTask(plugin, () -> {
            player.sendMessage(Component.text("Echo: " + content));
        });
    }
    super.channelRead(ctx, msg);
}

模式 2:非同步 IO → NMS 操作

CompletableFuture.supplyAsync(() -> database.fetchPlayerData(uuid))
    .thenAccept(data -> {
        // 回到主執行緒再存取 NMS
        Bukkit.getScheduler().runTask(plugin, () -> {
            ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
            serverPlayer.connection.send(buildPacket(data));
        });
    });

模式 3:封包發送(可跨執行緒)

ServerPlayer.connection.send() 內部會將封包排入 Netty write queue,可從任何執行緒呼叫,但封包內容建構若依賴世界狀態,必須在主執行緒完成。

// ✅ OK:封包在主執行緒建構,發送在任何執行緒
Bukkit.getScheduler().runTask(plugin, () -> {
    ClientboundSetTitleTextPacket packet = new ClientboundSetTitleTextPacket(
        Component.literal("Hello").getVisualOrderText()
    );
    CompletableFuture.runAsync(() -> serverPlayer.connection.send(packet));
});

模式 4:Tick 任務(NMS 原生)

繞過 Bukkit Scheduler,直接使用 NMS tick 佇列(更低延遲)。

import net.minecraft.server.MinecraftServer;

MinecraftServer.getServer().execute(() -> {
    // 下一個 tick 在主執行緒執行
    level.broadcastEntityEvent(entity, (byte) 6);
});

常見錯誤

錯誤 症狀 修正
Netty 執行緒呼叫 Player.teleport() ConcurrentModificationException 用 Bukkit.getScheduler().runTask() 切回主執行緒
Async thread 存取 Level.getChunk() Chunk 未載入或 race condition 先在主執行緒讀取 chunk data,再傳給 async
在 channelRead 內阻塞 所有該玩家的封包延遲 Netty handler 內不可呼叫阻塞 IO

相關技能

  • nms-packet-sender — 封包發送不同執行緒的正確姿勢
  • nms-packet-interceptor — Netty pipeline 注入與執行緒切換