ワンクリックで
multithreading
// Use when running work off the main thread — WorkerThreadPool, Thread/Mutex/Semaphore, call_deferred, thread-safe scene access, and threaded resource loading
// Use when running work off the main thread — WorkerThreadPool, Thread/Mutex/Semaphore, call_deferred, thread-safe scene access, and threaded resource loading
Use when building native extensions for Godot — godot-cpp (C++) or gdext (Rust), binding classes, building, and GDScript/C# interop
Use when targeting Android/iOS — export and signing, permissions, plugins, in-app purchases, ads, app lifecycle, device features, and mobile performance
Use when importing and managing assets — image compression, 3D scene import, audio formats, resource formats, and import configuration
Use when working with C# in Godot — conventions, GodotSharp API differences from GDScript, project setup, and interop
Use when exporting and distributing Godot games — export presets, platform settings, CI/CD with GitHub Actions
Use when optimizing Godot games — profiler, draw calls, physics tuning, memory management, and common bottlenecks
| name | multithreading |
| description | Use when running work off the main thread — WorkerThreadPool, Thread/Mutex/Semaphore, call_deferred, thread-safe scene access, and threaded resource loading |
Run expensive work off the main thread without corrupting the scene tree. Prefer WorkerThreadPool for short parallel jobs; reach for Thread/Mutex/Semaphore only when you need a long-lived worker.
Related skills: godot-optimization for profiling before threading, assets-pipeline for asset import, csharp-godot for C# specifics, gdscript-advanced for async/await pitfalls.
The main thread owns the scene tree — interacting with the active scene tree is not thread-safe. Observe these doc-sourced rules:
Rendering > Driver > Thread Model = Separate, Physics > {2D,3D} > Run on Separate Thread). Servers handle thousands of thread-driven instances well.Navigation > Pathfinding > Max Threads.Array/Dictionary: reading/writing existing elements across threads is OK; resizing (add/remove) needs a Mutex.add_child.call_deferred() — only with a single loader thread (multiple threads risk tweaking the same cached resource → crashes).Golden rule: Mutate the scene tree only on the main thread. From a worker, hand results back with
call_deferred/set_deferred.
WorkerThreadPool is a global singleton with threads allocated at startup. A regular task (add_task) runs on one worker; a group task (add_group_task) is distributed across workers, calling the Callable repeatedly for each element index — great for iterating many elements. Every task must be waited on (wait_for_task_completion / wait_for_group_task_completion) or its allocated resources leak. Distributing cheap work can hurt performance — only use it for genuinely expensive work.
var enemies = [] # Filled with enemies elsewhere.
func process_enemy_ai(enemy_index):
var processed_enemy = enemies[enemy_index]
# Expensive per-enemy logic...
func _process(delta):
var task_id = WorkerThreadPool.add_group_task(process_enemy_ai, enemies.size())
# ... other main-thread work ...
WorkerThreadPool.wait_for_group_task_completion(task_id)
# Safe to read results now.
private List<Node> _enemies = new(); // Filled with enemies elsewhere.
private void ProcessEnemyAI(int enemyIndex)
{
Node processedEnemy = _enemies[enemyIndex];
// Expensive per-enemy logic...
}
public override void _Process(double delta)
{
long taskId = WorkerThreadPool.AddGroupTask(Callable.From<int>(ProcessEnemyAI), _enemies.Count);
// ... other main-thread work ...
WorkerThreadPool.WaitForGroupTaskCompletion(taskId);
// Safe to read results now.
}
This relies on the element count staying constant during the multithreaded part.
Real signatures: Thread.start(callable: Callable, priority := PRIORITY_NORMAL), wait_to_finish() (blocks; join before free), is_alive(). Mutex is reentrant (lock/unlock/try_lock). Semaphore exposes wait() / post(count := 1).
The canonical semaphore producer/consumer + clean-shutdown idiom:
var counter := 0
var mutex: Mutex
var semaphore: Semaphore
var thread: Thread
var exit_thread := false
func _ready():
mutex = Mutex.new()
semaphore = Semaphore.new()
thread = Thread.new()
thread.start(_thread_function)
func _thread_function():
while true:
semaphore.wait() # Block until there is work.
mutex.lock()
var should_exit = exit_thread
mutex.unlock()
if should_exit:
break
mutex.lock()
counter += 1
mutex.unlock()
func increment_counter():
semaphore.post() # Wake the worker.
func _exit_tree():
mutex.lock()
exit_thread = true
mutex.unlock()
semaphore.post() # Unblock so it can see exit_thread.
thread.wait_to_finish() # Join.
Godot.Mutex/Godot.Semaphore also exist, but System.Threading is idiomatic in C#:
using Godot;
using System.Threading;
public partial class Worker : Node
{
private int _counter;
private readonly object _lock = new();
private readonly SemaphoreSlim _semaphore = new(0);
private Thread _thread;
private volatile bool _exitThread;
public override void _Ready()
{
_thread = new Thread(ThreadFunction) { IsBackground = true };
_thread.Start();
}
private void ThreadFunction()
{
while (true)
{
_semaphore.Wait(); // Block until there is work.
if (_exitThread) break;
lock (_lock) { _counter++; }
}
}
public void IncrementCounter() => _semaphore.Release(); // Wake the worker.
public override void _ExitTree()
{
_exitThread = true;
_semaphore.Release(); // Unblock so it can see _exitThread.
_thread.Join(); // Join.
}
}
Thread creation is slow (especially on Windows) — pre-create before heavy work, not just-in-time. Over-locking mutexes is also costly.
# Unsafe from a worker thread:
world.add_child(enemy)
# Safe:
world.add_child.call_deferred(enemy)
// Unsafe from a worker thread:
world.AddChild(enemy);
// Safe — use the MethodName StringName constant, NOT "AddChild":
world.CallDeferred(Node.MethodName.AddChild, enemy);
In C#, CallDeferred("AddChild") fails — the deferred/Call/Connect APIs use Godot's snake_case names. Prefer the Node.MethodName.* constants (avoids the pitfall and an allocation).
ResourceLoader.load_threaded_request(path) starts the load. Poll load_threaded_get_status(path, progress) each frame (progress[0] is the 0–1 ratio); on THREAD_LOAD_LOADED call load_threaded_get(path). load_threaded_get blocks like load() if the load is not finished — always poll first. Statuses: THREAD_LOAD_INVALID_RESOURCE / THREAD_LOAD_IN_PROGRESS / THREAD_LOAD_FAILED / THREAD_LOAD_LOADED.
const SCENE_PATH := "res://enemy.tscn"
var _progress: Array = []
func _ready():
ResourceLoader.load_threaded_request(SCENE_PATH)
func _process(_delta):
var status := ResourceLoader.load_threaded_get_status(SCENE_PATH, _progress)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
$ProgressBar.value = _progress[0] * 100.0
ResourceLoader.THREAD_LOAD_LOADED:
var scene: PackedScene = ResourceLoader.load_threaded_get(SCENE_PATH)
add_child(scene.instantiate())
set_process(false)
ResourceLoader.THREAD_LOAD_FAILED, ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
push_error("Threaded load failed: %s" % SCENE_PATH)
set_process(false)
private const string ScenePath = "res://enemy.tscn";
private readonly Godot.Collections.Array _progress = new();
public override void _Ready() => ResourceLoader.LoadThreadedRequest(ScenePath);
public override void _Process(double delta)
{
var status = ResourceLoader.LoadThreadedGetStatus(ScenePath, _progress);
switch (status)
{
case ResourceLoader.ThreadLoadStatus.InProgress:
GetNode<ProgressBar>("ProgressBar").Value = (double)_progress[0] * 100.0;
break;
case ResourceLoader.ThreadLoadStatus.Loaded:
var scene = (PackedScene)ResourceLoader.LoadThreadedGet(ScenePath);
AddChild(scene.Instantiate());
SetProcess(false);
break;
case ResourceLoader.ThreadLoadStatus.Failed:
case ResourceLoader.ThreadLoadStatus.InvalidResource:
GD.PushError($"Threaded load failed: {ScenePath}");
SetProcess(false);
break;
}
}
In C#, prefer System.Threading.Tasks.Task.Run / async-await for fire-and-forget CPU work; never touch Godot objects or await ToSignal(...) from a background thread — marshal results back with CallDeferred. Use WorkerThreadPool when you want Godot's pool and engine integration; use Task when you want .NET idioms. (GDScript users: use WorkerThreadPool or Thread from the sections above.)
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("compute"))
{
_ = System.Threading.Tasks.Task.Run(() =>
{
int result = ExpensiveComputation(); // Pure CPU work, no Godot objects.
CallDeferred(MethodName.OnComputed, result); // Marshal back to main thread.
});
}
}
private void OnComputed(int result) => GD.Print($"Done: {result}");
Deeper: see Pitfalls & deadlocks for data races, the
ERR_BUSYnested-wait deadlock, and when threading hurts.
call_deferred / set_deferred)WorkerThreadPool task is waited on (wait_for_*_completion)Mutex / lock; container resizes are lockedwait_to_finish / Join) before the owning node freesload_threaded_get