/multithreading
Use when running work off the main thread — WorkerThreadPool, Thread/Mutex/Semaphore, call_deferred, thread-safe scene access, and threaded resource loading
$ npx -y skills add jame581/GodotPrompter --skill multithreading --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/multithreading
Context preview
The summary Claude sees to decide when to auto-load this skill.
Use when running work off the main thread — WorkerThreadPool, Thread/Mutex/Semaphore, call_deferred, thread-safe scene access, and threaded resource loading
SKILL.md
multithreading.SKILL.mdname: multithreading
description: Use when running work off the main thread — WorkerThreadPool, Thread/Mutex/Semaphore, call_deferred, thread-safe scene access, and threaded resource loading
Multithreading
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.
---
1. Threading model & safety rules
The main thread owns the scene tree — **interacting with the active scene tree is not thread-safe.** Observe these doc-sourced rules:
- **Servers** (RenderingServer, PhysicsServer) are thread-safe **only after enabling it in Project Settings** (`Rendering > Driver > Thread Model = Separate`, `Physics > {2D,3D} > Run on Separate Thread`). Servers handle thousands of thread-driven instances well.
- **NavigationServer2D/3D are thread-safe and thread-friendly** (true parallel queries); tune `Navigation > Pathfinding > Max Threads`.
- **AStar2D/3D/Grid2D are NOT thread-safe** — one dedicated thread per object only; sharing one object across threads corrupts data.
- **GDScript `Array`/`Dictionary`:** reading/writing existing elements across threads is OK; **resizing (add/remove) needs a `Mutex`.**
- **No GPU work off the main thread** (texture creation, image read/modify) — causes RenderingServer sync stalls.
- **Build scene chunks off-tree** in a thread, then add them on the main thread via `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`.
---
2. WorkerThreadPool (preferred)
`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.
GDScript
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.C# Equivalent
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.
---
3. Thread / Mutex / Semaphore
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)`.
GDScript
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.C# Equivalent
`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.
---
4. Handing results back: call_deferred / set_deferred
GDScript
# Unsafe from a worker thread:
world.add_chil
Read more
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
Multithreading
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.
---
1. Threading model & safety rules
The main thread owns the scene tree — **interacting with the active scene tree is not thread-safe.** Observe these doc-sourced rules:
- **Servers** (RenderingServer, PhysicsServer) are thread-safe **only after enabling it in Project Settings** (`Rendering > Driver > Thread Model = Separate`, `Physics > {2D,3D} > Run on Separate Thread`). Servers handle thousands of thread-driven instances well.
- **NavigationServer2D/3D are thread-safe and thread-friendly** (true parallel queries); tune `Navigation > Pathfinding > Max Threads`.
- **AStar2D/3D/Grid2D are NOT thread-safe** — one dedicated thread per object only; sharing one object across threads corrupts data.
- **GDScript `Array`/`Dictionary`:** reading/writing existing elements across threads is OK; **resizing (add/remove) needs a `Mutex`.**
- **No GPU work off the main thread** (texture creation, image read/modify) — causes RenderingServer sync stalls.
- **Build scene chunks off-tree** in a thread, then add them on the main thread via `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`.
---
2. WorkerThreadPool (preferred)
`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.
GDScript
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.C# Equivalent
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.
---
3. Thread / Mutex / Semaphore
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)`.
GDScript
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.C# Equivalent
`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.
---
4. Handing results back: call_deferred / set_deferred
GDScript
# Unsafe from a worker thread: world.add_chil
Agentic skills framework for Godot 4.x game development. Gives AI coding agents domain-specific expertise for GDScript and C# projects.
Other skills on godot-prompter.
- /authoring-godot-prompter-skills
Use when writing or editing a SKILL.md or an agent definition in this repo — required frontmatter, section ordering, and the GDScript-then-C# example convention.
Open skill - /releasing-godot-prompter
Use when cutting a GodotPrompter release or bumping its version — the version-bump sequence, tag-triggered workflow, and the marketplace manifests that must follow.
Open skill - /2d-essentials
Use when working with 2D-specific systems — TileMaps, parallax scrolling, 2D lights and shadows, canvas layers, particles 2D, custom drawing, and 2D meshes in Godot 4.3+
Open skill - /3d-essentials
Use when working with 3D-specific systems — materials, lighting, shadows, environment, global illumination, fog, LOD, occlusion culling, and decals in Godot 4.3+
Open skill - /ability-system
Use when building character abilities — Resource-based abilities with cost/cooldown/cast, buffs/debuffs, stat modifiers, gameplay tags, and HUD binding
Open skill - /addon-development
Use when creating Godot editor plugins — EditorPlugin, @tool scripts, custom inspectors, and dock panels
Open skill

