Threads, Mutexes & WorkerThreadPool
Use threads for heavy work that would otherwise freeze the game: procedural generation, pathfinding over huge graphs, file I/O, decompression.
34.1 When to use threads
Use threads for heavy work that would otherwise freeze the game: procedural generation, pathfinding over huge graphs, file I/O, decompression. The scene tree is not thread-safe: don't add/remove/modify nodes in the tree from other threads — compute data on the thread, then apply it on the main thread with call_deferred().
34.2 Thread
thread.start(callable, priority) returns an Error; wait_to_finish() returns the callable's return value.
var thread := Thread.new()
func _ready() -> void:
thread.start(_generate_world.bind(12345))
func _generate_world(seed_value: int) -> void:
var rng := RandomNumberGenerator.new()
rng.seed = seed_value
var heights := PackedFloat32Array()
heights.resize(512 * 512)
for i in heights.size():
heights[i] = rng.randf()
_apply_world.call_deferred(heights) # back to the main thread
func _apply_world(heights: PackedFloat32Array) -> void:
print("World ready: ", heights.size())
func _exit_tree() -> void:
if thread.is_started():
thread.wait_to_finish() # always join threads34.3 Mutex and Semaphore
mutex.try_lock() returns true if it acquired the lock without blocking.
var mutex := Mutex.new()
var semaphore := Semaphore.new()
var queue: Array = []
var running := true
var worker := Thread.new()
func _ready() -> void:
worker.start(_worker_loop)
func push_job(job) -> void:
mutex.lock()
queue.append(job)
mutex.unlock()
semaphore.post() # wake the worker
func _worker_loop() -> void:
while true:
semaphore.wait() # sleep until there's work
mutex.lock()
var should_exit := not running
var job = queue.pop_front() if not queue.is_empty() else null
mutex.unlock()
if should_exit: break
if job != null: _process_job(job)
func _process_job(job) -> void:
pass
func _exit_tree() -> void:
mutex.lock(); running = false; mutex.unlock()
semaphore.post()
worker.wait_to_finish()34.4 WorkerThreadPool
A built-in pool — simpler than managing threads yourself:
is_task_completed(id) / is_group_task_completed(id) let you poll instead of blocking. Note that wait functions block the calling thread.
var results := PackedFloat32Array()
func _ready() -> void:
# Single task
var id := WorkerThreadPool.add_task(_heavy_task)
# ... do other things ...
WorkerThreadPool.wait_for_task_completion(id)
# Group task: the callable runs once per index, in parallel
results.resize(1000)
var group := WorkerThreadPool.add_group_task(_compute_element, results.size())
WorkerThreadPool.wait_for_group_task_completion(group)
print(results[999])
func _heavy_task() -> void:
pass
func _compute_element(index: int) -> void:
results[index] = sqrt(float(index)) # each index writes its own slot: no lock needed34.5 Thread-safety tips
Safe from threads Your own data (with mutexes) Adding/removing/changing nodes in the active tree
Most Node methods on nodes inside the tree Servers (many functions are thread-safe if *Thread Model* allows)
Signals that touch the tree (emit via call_deferred) Creating nodes not yet in the tree
Accessing the same Array/Dictionary from two threads without locking
Debug thread issues with the debugger's thread selector and keep threaded code small and well-isolated.