Skip to content
Development
Skill

/multithreading

Use when running work off the main thread — WorkerThreadPool, Thread/Mutex/Semaphore, call_deferred, thread-safe scene access, and threaded resource loading

From plugin
godot-prompter
54157 skills9 agents1 hook
Install
$ npx -y skills add jame581/GodotPrompter --skill multithreading --agent claude-code

How 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.md
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
Read more
Ships withgodot-prompter

Agentic skills framework for Godot 4.x game development. Gives AI coding agents domain-specific expertise for GDScript and C# projects.

Get the whole plugin