Threading — WorkerThreadPool and Thread
Parallel tasks in Godot — map generation, data processing, long operations without freezing the UI.
In Godot the main thread handles rendering, physics, and scripts. If you procedurally generate a
world map in _ready, the game will freeze for a second or two. Threading is the way to move
heavy work to background threads.
Three levels of tools
| Node/class | Purpose | When |
|---|---|---|
WorkerThreadPool | A pool of ready worker threads, one per CPU core | 80% of cases — recommended |
Thread | A single manual thread | When you need full control or long-running work |
Mutex / Semaphore | Synchronization between threads | Anywhere with shared data |
WorkerThreadPool appeared in Godot 4 and is the main path. It manages the threads for you; you just
submit tasks.
Web Workers + SharedArrayBuffer. The difference: a Godot Thread runs at the native level, without serialization overhead.
WorkerThreadPool — basic example
Task: procedurally generate a 1024×1024 heightmap.
extends Node
var _task_id: int = -1
var _heightmap: PackedFloat32Array
func _ready() -> void:
# Launch the heavy task in the background.
_task_id = WorkerThreadPool.add_task(_generate_heightmap, true)
# The second argument is high_priority. This task goes to the front of the queue.
func _process(_delta: float) -> void:
if _task_id == -1:
return
# Don't block the main thread. Check whether the task is done.
if WorkerThreadPool.is_task_completed(_task_id):
WorkerThreadPool.wait_for_task_completion(_task_id)
_on_heightmap_ready()
_task_id = -1
func _generate_heightmap() -> void:
# This method runs on a background thread.
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())
# Apply the result here — the main thread
The main WorkerThreadPool methods:
add_task(callable, high_priority = false, description = "")— submit, returns atask_id.is_task_completed(task_id)— check whether it’s done.wait_for_task_completion(task_id)— block the main thread until it finishes (called afteris_task_completedfor cleanup).add_group_task(callable, elements, tasks_needed, ...)— a parallel “for each”.
Parallel for-each via a group task
The equivalent of IJobParallelFor in Unity — add_group_task divides N iterations among the pool’s
workers:
extends Node
var _agents: Array[Node3D] = []
func _ready() -> void:
# 1000 agents, each an independent update
var group_id := WorkerThreadPool.add_group_task(
_update_agent,
_agents.size(), # tasks_needed
-1, # tasks_needed (-1 = by core count)
false, # high_priority
"Agent batch update"
)
WorkerThreadPool.wait_for_group_task_completion(group_id)
func _update_agent(index: int) -> void:
# Runs in parallel for different index values. The main rule:
# each index touches ONLY its own agent, otherwise a race condition.
var agent := _agents[index]
var new_pos := agent.global_position + Vector3.RIGHT
# ! You must NOT change the Node API from a thread. You can only compute values.
# Apply them on the main thread.
Most of the Godot API (Node, Transform3D, Image, RenderingServer) is not thread-safe. Modifying nodes or assets from a background thread is a path to a crash or undefined behavior. On a background thread do only computations over data (arrays, PackedArray, your custom structs), and apply the results on the main thread via a signal or a flag.
Exceptions that are explicitly thread-safe: ResourceLoader.load_threaded_*, the physics server with
explicit mutexes.
Mutex — protecting shared data
If several workers write to a shared array, you need a 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()
Without lock, two parallel _calc calls could .append at the same time, corrupting the array’s
internal state.
Alternative: give each task its own output array, then merge them on the main thread. This is faster than a mutex — there’s no contention.
Thread — a manual thread
When you need a long-lived background thread (a long-poll server, audio streaming, a custom job
queue), use Thread:
extends Node
var _thread: Thread
func _ready() -> void:
_thread = Thread.new()
_thread.start(_background_loop)
func _background_loop() -> void:
while not _should_stop:
# heavy work
OS.delay_msec(100)
var _should_stop: bool = false
func _exit_tree() -> void:
_should_stop = true
_thread.wait_to_finish()
Unlike WorkerThreadPool, Thread creates a new thread on every start — there’s no pool.
Use it only if the task stays alive for a long time.
Resource loading on a thread
The main scenario where threading is “mandatory” is 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)
Internally Godot creates its own thread for loading. More details in the Resource loading chapter.
C# in Godot — threading
In the C# variant of Godot you can use the standard .NET threading tools: Task.Run, Parallel.For,
async/await, lock. The same restrictions apply: the Godot Node API is main-thread only.
await Task.Run(() => {
// heavy work in the background
});
// here — back on the main thread, apply the result
When it’s worth moving to a thread
- Procedural generation of levels, textures, meshes.
- Pathfinding for a large number of agents (if the NavigationServer is overloaded).
- Parsing large files (JSON, binary formats).
- Network requests with a long response (although
HTTPRequestis already asynchronous). - Audio analysis / DSP in real time.
When it’s NOT worth it
- Lightweight operations (< 1 ms) — the overhead of creation/synchronization eats up the gain.
- A constant stream of data into the Node API — the main thread becomes a bottleneck anyway.
- Prototypes — it adds complexity with no visible benefit.