Threading — WorkerThreadPool и Thread
Параллельные задачи в Godot — генерация карт, обработка данных, длинные операции без фриза UI.
В Godot main-thread обрабатывает рендер, физику, скрипты. Если в _ready процедурно сгенерировать
карту мира — игра зависнет на секунду-две. Threading — способ вынести тяжёлую работу в
фоновые потоки.
Три уровня инструментов
| Узел/класс | Назначение | Когда |
|---|---|---|
WorkerThreadPool | Pool готовых worker-потоков, по числу ядер CPU | 80% случаев — рекомендован |
Thread | Один ручной поток | Если нужен полный контроль или long-running |
Mutex / Semaphore | Синхронизация между потоками | Везде, где shared data |
WorkerThreadPool появился в Godot 4 и — главный путь. Он управляет потоками за вас, вы только
submit-ите задачи.
Web Workers + SharedArrayBuffer. Только Godot Thread работает на native-уровне, без serialization overhead.
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 (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.
- Прототипы — добавит сложности без видимой пользы.