~2 мин чтения

Threading — WorkerThreadPool и Thread

Параллельные задачи в Godot — генерация карт, обработка данных, длинные операции без фриза UI.

В Godot main-thread обрабатывает рендер, физику, скрипты. Если в _ready процедурно сгенерировать карту мира — игра зависнет на секунду-две. Threading — способ вынести тяжёлую работу в фоновые потоки.

Три уровня инструментов

Узел/классНазначениеКогда
WorkerThreadPoolPool готовых worker-потоков, по числу ядер CPU80% случаев — рекомендован
ThreadОдин ручной потокЕсли нужен полный контроль или long-running
Mutex / SemaphoreСинхронизация между потокамиВезде, где shared data

WorkerThreadPool появился в Godot 4 и — главный путь. Он управляет потоками за вас, вы только submit-ите задачи.

Веб

Web Workers + SharedArrayBuffer. Только Godot Thread работает на native-уровне, без serialization overhead.

Unity

WorkerThreadPool — базовый пример

Задача: процедурно сгенерировать heightmap 1024×1024.

extends Node

var _task_id: int = -1
var _heightmap: PackedFloat32Array

func _ready() -> void:
    # Запускаем тяжёлую задачу в фоне.
    _task_id = WorkerThreadPool.add_task(_generate_heightmap, true)
    # Второй аргумент — high_priority. Этот таск пойдёт впереди очереди.

func _process(_delta: float) -> void:
    if _task_id == -1:
        return
    # Не блокируем main-thread. Проверяем, готова ли задача.
    if WorkerThreadPool.is_task_completed(_task_id):
        WorkerThreadPool.wait_for_task_completion(_task_id)
        _on_heightmap_ready()
        _task_id = -1

func _generate_heightmap() -> void:
    # Этот метод выполняется в фоновом потоке.
    var data := PackedFloat32Array()
    data.resize(1024 * 1024)
    for y in 1024:
        for x in 1024:
            data[y * 1024 + x] = sin(x * 0.05) * cos(y * 0.05)
    _heightmap = data

func _on_heightmap_ready() -> void:
    print("Heightmap generated! Size: ", _heightmap.size())
    # Здесь применяем результат — главный поток

Главные методы WorkerThreadPool:

  • add_task(callable, high_priority = false, description = "") — submit, возвращает task_id.
  • is_task_completed(task_id) — проверить готовность.
  • wait_for_task_completion(task_id) — заблокировать main, пока не закончится (вызывается после is_task_completed для cleanup).
  • add_group_task(callable, elements, tasks_needed, ...) — параллельное “for each”.

Параллельный for-each через group task

Аналог IJobParallelFor в Unity — add_group_task делит N итераций на worker-ы пула:

extends Node

var _agents: Array[Node3D] = []

func _ready() -> void:
    # 1000 агентов, каждый — независимое обновление
    var group_id := WorkerThreadPool.add_group_task(
        _update_agent,
        _agents.size(),      # tasks_needed
        -1,                  # tasks_needed (-1 = по числу ядер)
        false,               # high_priority
        "Agent batch update"
    )
    WorkerThreadPool.wait_for_group_task_completion(group_id)

func _update_agent(index: int) -> void:
    # Выполняется параллельно для разных index. Главное правило:
    # каждый index трогает ТОЛЬКО свой агент, иначе race condition.
    var agent := _agents[index]
    var new_pos := agent.global_position + Vector3.RIGHT
    # ! НЕЛЬЗЯ менять Node API из потока. Можно только посчитать значения.
    # Применить — на main thread.
Godot API — main-thread only

Большинство Godot API (Node, Transform3D, Image, RenderingServer) не thread-safe. Изменять узлы или ассеты из фонового потока — путь к крашу или undefined behavior. В фоне делайте только вычисления над данными (массивы, PackedArray, ваши custom struct’ы), результаты применяйте на main-thread через сигнал или флаг.

Исключения, явно безопасные для потоков: ResourceLoader.load_threaded_*, физический сервер с явными mutex’ами.

Mutex — защита shared data

Если несколько worker-ов пишут в общий массив, нужен Mutex:

extends Node

var _results: Array = []
var _mutex := Mutex.new()

func _calc(index: int) -> void:
    var value := index * 2.0
    _mutex.lock()
    _results.append(value)
    _mutex.unlock()

Без lock две параллельные _calc могут одновременно .append, повредив внутреннее состояние массива.

Альтернатива: дать каждому task свой output-array, потом мерджить на main thread. Это быстрее mutex’а — нет contention.

Thread — ручной поток

Когда нужен долгоживущий background-thread (long-poll server, audio streaming, custom job queue), используйте Thread:

extends Node

var _thread: Thread

func _ready() -> void:
    _thread = Thread.new()
    _thread.start(_background_loop)

func _background_loop() -> void:
    while not _should_stop:
        # тяжёлая работа
        OS.delay_msec(100)

var _should_stop: bool = false

func _exit_tree() -> void:
    _should_stop = true
    _thread.wait_to_finish()

В отличие от WorkerThreadPool, Thread создаёт новый поток на каждый старт — нет пула. Используйте только если задача live долго.

Resource loading в потоке

Главный сценарий, для которого threading “обязателен” — это ResourceLoader.load_threaded_request:

ResourceLoader.load_threaded_request("res://big_scene.tscn")

func _process(_delta: float) -> void:
    var status = ResourceLoader.load_threaded_get_status("res://big_scene.tscn")
    match status:
        ResourceLoader.THREAD_LOAD_LOADED:
            var scene = ResourceLoader.load_threaded_get("res://big_scene.tscn")
            get_tree().change_scene_to_packed(scene)

Внутри Godot создаёт свой поток для загрузки. Подробнее — в главе Загрузка ресурсов.

C# в Godot — threading

В C#-варианте Godot можно использовать стандартные .NET-thread-инструменты: Task.Run, Parallel.For, async/await, lock. Те же ограничения: Godot Node API — main-thread only.

await Task.Run(() => {
    // тяжёлая работа в фоне
});
// здесь — снова main thread, применяем результат

Когда стоит выносить в поток

  • Процедурная генерация уровней, текстур, mesh’ей.
  • Pathfinding для большого количества агентов (если NavigationServer перегружен).
  • Парсинг больших файлов (JSON, бинарные форматы).
  • Сетевые запросы с долгим ответом (хотя HTTPRequest уже асинхронен).
  • Audio analysis / DSP в реальном времени.

Когда НЕ стоит

  • Лёгкие операции (< 1 мс) — overhead создания/синхронизации съест выигрыш.
  • Постоянный поток данных в Node API — main thread всё равно станет bottleneck.
  • Прототипы — добавит сложности без видимой пользы.