Thread Safety - Concurrent Programming Guide

Introduction

Safe concurrent programming in GDExtension: Threading safety is critical when building GDExtensions that use multiple threads or interact with Godot’s threaded subsystems. This guide covers thread-safe patterns, synchronization techniques, and best practices for avoiding race conditions and deadlocks.

Understanding Godot’s Threading Context

When and where your GDExtension code runs: Godot uses multiple threads for different purposes, and understanding which thread context your code executes in is essential for thread safety.

Thread Contexts in Godot

class ThreadContextExample : public Node {
    GDCLASS(ThreadContextExample, Node)

private:
    std::thread::id main_thread_id;
    std::atomic<int> call_count{0};
    std::mutex log_mutex;

public:
    ThreadContextExample() {
        main_thread_id = std::this_thread::get_id();
    }

    void _ready() override {
        log_thread_context("_ready called");

        // Main thread operations are safe
        modify_scene_tree();
    }

    void _process(double delta) override {
        log_thread_context("_process called");

        // This runs on the main thread - safe for most operations
        update_ui_elements();
    }

    void _physics_process(double delta) override {
        log_thread_context("_physics_process called");

        // This might run on a physics thread - be careful!
        update_physics_state();
    }

    // Called from audio thread
    virtual void _mix_audio(AudioFrame* buffer, int frame_count) {
        log_thread_context("_mix_audio called");

        // Audio thread - very restrictive!
        // No Godot API calls, minimal allocations
        process_audio_safely(buffer, frame_count);
    }

private:
    void log_thread_context(const String& context) {
        std::lock_guard<std::mutex> lock(log_mutex);

        std::thread::id current_id = std::this_thread::get_id();
        bool is_main_thread = (current_id == main_thread_id);

        call_count.fetch_add(1);

        // Safe to print from main thread, be careful from others
        if (is_main_thread) {
            UtilityFunctions::print(context, "- Main thread, call", call_count.load());
        }
    }

    void modify_scene_tree() {
        // Scene tree operations - MAIN THREAD ONLY
        Node* child = memnew(Node);
        child->set_name("ThreadSafeChild");
        add_child(child);
    }

    void update_ui_elements() {
        // UI updates - MAIN THREAD ONLY
        Control* ui = Object::cast_to<Control>(get_node_or_null(NodePath("UI")));
        if (ui) {
            // ui->set_size(Vector2(100, 100));  // Safe on main thread
        }
    }

    void update_physics_state() {
        // Physics state - potentially physics thread
        // Use atomic operations or proper synchronization
        call_count.fetch_add(1);
    }

    void process_audio_safely(AudioFrame* buffer, int frame_count) {
        // Audio processing - audio thread
        // NO Godot API calls, NO memory allocation, NO complex operations

        for (int i = 0; i < frame_count; i++) {
            // Simple audio processing only
            buffer[i].l *= 0.5f;
            buffer[i].r *= 0.5f;
        }
    }
};

Thread-Safe Communication Patterns

class ThreadSafeCommunication : public Node {
    GDCLASS(ThreadSafeCommunication, Node)

private:
    // Thread-safe data structures
    std::atomic<bool> worker_running{false};
    std::thread worker_thread;

    // Message queue for thread communication
    std::queue<String> message_queue;
    std::mutex queue_mutex;
    std::condition_variable queue_cv;

    // Main thread command queue
    std::queue<std::function<void()>> main_thread_commands;
    std::mutex command_mutex;

protected:
    static void _bind_methods() {
        ClassDB::bind_method(D_METHOD("start_worker"), &ThreadSafeCommunication::start_worker);
        ClassDB::bind_method(D_METHOD("stop_worker"), &ThreadSafeCommunication::stop_worker);
        ClassDB::bind_method(D_METHOD("send_message_to_worker", "message"),
                           &ThreadSafeCommunication::send_message_to_worker);
        ClassDB::bind_method(D_METHOD("process_main_thread_commands"),
                           &ThreadSafeCommunication::process_main_thread_commands);
    }

public:
    ~ThreadSafeCommunication() {
        stop_worker();
    }

    void start_worker() {
        if (worker_running.load()) {
            return;  // Already running
        }

        worker_running.store(true);
        worker_thread = std::thread([this]() {
            worker_loop();
        });
    }

    void stop_worker() {
        worker_running.store(false);

        // Wake up worker thread
        queue_cv.notify_all();

        if (worker_thread.joinable()) {
            worker_thread.join();
        }
    }

    void send_message_to_worker(const String& message) {
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            message_queue.push(message);
        }
        queue_cv.notify_one();
    }

    void _process(double delta) override {
        // Process commands that need to run on main thread
        process_main_thread_commands();
    }

    void process_main_thread_commands() {
        std::lock_guard<std::mutex> lock(command_mutex);

        while (!main_thread_commands.empty()) {
            auto command = main_thread_commands.front();
            main_thread_commands.pop();

            // Execute command on main thread
            command();
        }
    }

private:
    void worker_loop() {
        while (worker_running.load()) {
            String message;

            // Wait for message
            {
                std::unique_lock<std::mutex> lock(queue_mutex);
                queue_cv.wait(lock, [this] {
                    return !message_queue.empty() || !worker_running.load();
                });

                if (!worker_running.load()) {
                    break;
                }

                if (!message_queue.empty()) {
                    message = message_queue.front();
                    message_queue.pop();
                }
            }

            if (!message.is_empty()) {
                process_worker_message(message);
            }
        }
    }

    void process_worker_message(const String& message) {
        // Worker thread processing
        std::this_thread::sleep_for(std::chrono::milliseconds(100));  // Simulate work

        // Send result back to main thread
        queue_main_thread_command([this, message]() {
            // This will execute on main thread
            UtilityFunctions::print("Worker processed:", message);

            // Safe to call Godot APIs here
            Node* result_node = memnew(Node);
            result_node->set_name("WorkerResult_" + message);
            add_child(result_node);
        });
    }

    void queue_main_thread_command(std::function<void()> command) {
        std::lock_guard<std::mutex> lock(command_mutex);
        main_thread_commands.push(command);
    }
};

Synchronization Primitives

Atomic Operations

class AtomicOperationsExample : public RefCounted {
    GDCLASS(AtomicOperationsExample, RefCounted)

private:
    // Atomic primitives for lock-free operations
    std::atomic<int> counter{0};
    std::atomic<float> average_value{0.0f};
    std::atomic<bool> is_processing{false};
    std::atomic<uint64_t> total_bytes_processed{0};

    // Atomic pointers
    std::atomic<Node*> current_target{nullptr};

protected:
    static void _bind_methods() {
        ClassDB::bind_method(D_METHOD("increment_counter"), &AtomicOperationsExample::increment_counter);
        ClassDB::bind_method(D_METHOD("get_counter"), &AtomicOperationsExample::get_counter);
        ClassDB::bind_method(D_METHOD("update_average", "new_value"), &AtomicOperationsExample::update_average);
        ClassDB::bind_method(D_METHOD("try_start_processing"), &AtomicOperationsExample::try_start_processing);
    }

public:
    void increment_counter() {
        // Thread-safe increment
        counter.fetch_add(1, std::memory_order_relaxed);
    }

    int get_counter() const {
        return counter.load(std::memory_order_relaxed);
    }

    void update_average(float new_value) {
        // Lock-free update using compare-and-swap
        float expected = average_value.load();
        float new_avg;

        do {
            new_avg = (expected + new_value) / 2.0f;
        } while (!average_value.compare_exchange_weak(expected, new_avg,
                                                     std::memory_order_release,
                                                     std::memory_order_relaxed));
    }

    bool try_start_processing() {
        // Atomic test-and-set pattern
        bool expected = false;
        return is_processing.compare_exchange_strong(expected, true,
                                                   std::memory_order_acquire);
    }

    void finish_processing() {
        is_processing.store(false, std::memory_order_release);
    }

    void add_bytes_processed(uint64_t bytes) {
        total_bytes_processed.fetch_add(bytes, std::memory_order_relaxed);
    }

    uint64_t get_total_bytes_processed() const {
        return total_bytes_processed.load(std::memory_order_relaxed);
    }

    void set_target(Node* target) {
        current_target.store(target, std::memory_order_release);
    }

    Node* get_target() const {
        return current_target.load(std::memory_order_acquire);
    }

    // Atomic operations with memory ordering
    void demonstrate_memory_ordering() {
        // Relaxed ordering - no synchronization, only atomicity
        counter.fetch_add(1, std::memory_order_relaxed);

        // Acquire-release ordering - synchronizes with paired operations
        is_processing.store(true, std::memory_order_release);  // Release
        bool processing = is_processing.load(std::memory_order_acquire);  // Acquire

        // Sequential consistency - strongest ordering (default)
        counter.fetch_add(1);  // Equivalent to memory_order_seq_cst
    }
};

Mutex and Lock Patterns

class MutexPatterns : public RefCounted {
    GDCLASS(MutexPatterns, RefCounted)

private:
    // Different mutex types for different needs
    std::mutex exclusive_mutex;
    std::recursive_mutex recursive_mutex;
    std::shared_mutex shared_mutex;
    std::timed_mutex timed_mutex;

    // Protected data
    Vector<String> shared_list;
    HashMap<String, int> shared_map;
    int recursive_counter = 0;

protected:
    static void _bind_methods() {
        ClassDB::bind_method(D_METHOD("add_item", "item"), &MutexPatterns::add_item);
        ClassDB::bind_method(D_METHOD("get_items"), &MutexPatterns::get_items);
        ClassDB::bind_method(D_METHOD("update_map", "key", "value"), &MutexPatterns::update_map);
        ClassDB::bind_method(D_METHOD("recursive_operation", "depth"), &MutexPatterns::recursive_operation);
    }

public:
    void add_item(const String& item) {
        // Exclusive access for writing
        std::lock_guard<std::mutex> lock(exclusive_mutex);
        shared_list.append(item);

        UtilityFunctions::print("Added item:", item, "Total:", shared_list.size());
    }

    Array get_items() {
        // Shared access for reading
        std::shared_lock<std::shared_mutex> lock(shared_mutex);

        Array result;
        for (const String& item : shared_list) {
            result.append(item);
        }
        return result;
    }

    void update_map(const String& key, int value) {
        // Try to acquire lock with timeout
        if (timed_mutex.try_lock_for(std::chrono::milliseconds(100))) {
            shared_map[key] = value;
            timed_mutex.unlock();
            UtilityFunctions::print("Updated map:", key, "=", value);
        } else {
            WARN_PRINT("Failed to acquire lock for map update");
        }
    }

    void recursive_operation(int depth) {
        // Recursive mutex allows same thread to lock multiple times
        std::lock_guard<std::recursive_mutex> lock(recursive_mutex);

        recursive_counter++;
        UtilityFunctions::print("Recursive operation depth:", depth, "counter:", recursive_counter);

        if (depth > 0) {
            recursive_operation(depth - 1);  // Recursive call with same lock
        }
    }

    // RAII lock management
    void demonstrate_raii_locking() {
        {
            std::lock_guard<std::mutex> auto_lock(exclusive_mutex);
            // Lock is automatically acquired

            // Do work...
            shared_list.append("RAII example");

            // Lock is automatically released when auto_lock goes out of scope
        }

        // Manual lock management (not recommended)
        exclusive_mutex.lock();
        try {
            shared_list.append("Manual locking");
            exclusive_mutex.unlock();
        } catch (...) {
            exclusive_mutex.unlock();  // Must unlock in exception case
            throw;
        }
    }

    // Multiple lock acquisition (deadlock prevention)
    void transfer_data(MutexPatterns* other) {
        if (!other) return;

        // Use std::lock to acquire multiple mutexes atomically
        std::lock(exclusive_mutex, other->exclusive_mutex);

        // Adopt the already-acquired locks
        std::lock_guard<std::mutex> lock1(exclusive_mutex, std::adopt_lock);
        std::lock_guard<std::mutex> lock2(other->exclusive_mutex, std::adopt_lock);

        // Safe to access both objects' data
        String item = shared_list.is_empty() ? "" : shared_list[0];
        if (!item.is_empty()) {
            other->shared_list.append(item);
            shared_list.remove_at(0);
        }
    }

    // Conditional locking
    bool try_quick_operation() {
        if (exclusive_mutex.try_lock()) {
            // Got the lock, do quick operation
            shared_list.append("Quick operation");
            exclusive_mutex.unlock();
            return true;
        } else {
            // Couldn't get lock, skip operation
            return false;
        }
    }

    // Scoped locking with custom RAII
    class ScopedOperationLock {
        MutexPatterns* owner;

    public:
        ScopedOperationLock(MutexPatterns* p_owner) : owner(p_owner) {
            owner->exclusive_mutex.lock();
            UtilityFunctions::print("Operation started");
        }

        ~ScopedOperationLock() {
            owner->exclusive_mutex.unlock();
            UtilityFunctions::print("Operation completed");
        }
    };

    void complex_operation() {
        ScopedOperationLock operation_lock(this);

        // Do complex work with automatic cleanup
        shared_list.append("Complex operation result");

        // Lock is automatically released when operation_lock goes out of scope
    }
};

Lock-Free Data Structures

Lock-Free Queue

template<typename T, size_t Size>
class LockFreeQueue {
private:
    struct alignas(64) Node {  // Cache line alignment
        std::atomic<T> data;
        std::atomic<bool> ready{false};
    };

    alignas(64) std::atomic<size_t> write_index{0};
    alignas(64) std::atomic<size_t> read_index{0};
    Node nodes[Size];

public:
    bool try_enqueue(const T& item) {
        size_t write_pos = write_index.load(std::memory_order_relaxed);
        size_t next_write = (write_pos + 1) % Size;

        // Check if queue is full
        if (next_write == read_index.load(std::memory_order_acquire)) {
            return false;
        }

        // Store the data
        nodes[write_pos].data.store(item, std::memory_order_relaxed);
        nodes[write_pos].ready.store(true, std::memory_order_release);

        // Update write index
        write_index.store(next_write, std::memory_order_release);
        return true;
    }

    bool try_dequeue(T& item) {
        size_t read_pos = read_index.load(std::memory_order_relaxed);

        // Check if queue is empty
        if (read_pos == write_index.load(std::memory_order_acquire)) {
            return false;
        }

        // Wait for data to be ready
        while (!nodes[read_pos].ready.load(std::memory_order_acquire)) {
            std::this_thread::yield();
        }

        // Get the data
        item = nodes[read_pos].data.load(std::memory_order_relaxed);
        nodes[read_pos].ready.store(false, std::memory_order_relaxed);

        // Update read index
        read_index.store((read_pos + 1) % Size, std::memory_order_release);
        return true;
    }

    size_t size() const {
        size_t write_pos = write_index.load(std::memory_order_acquire);
        size_t read_pos = read_index.load(std::memory_order_acquire);

        if (write_pos >= read_pos) {
            return write_pos - read_pos;
        } else {
            return Size - read_pos + write_pos;
        }
    }

    bool empty() const {
        return read_index.load(std::memory_order_acquire) ==
               write_index.load(std::memory_order_acquire);
    }

    bool full() const {
        size_t write_pos = write_index.load(std::memory_order_acquire);
        size_t next_write = (write_pos + 1) % Size;
        return next_write == read_index.load(std::memory_order_acquire);
    }
};

class LockFreeExample : public Node {
    GDCLASS(LockFreeExample, Node)

private:
    LockFreeQueue<String, 1024> message_queue;
    std::thread producer_thread;
    std::thread consumer_thread;
    std::atomic<bool> running{false};

protected:
    static void _bind_methods() {
        ClassDB::bind_method(D_METHOD("start_lock_free_test"),
                           &LockFreeExample::start_lock_free_test);
        ClassDB::bind_method(D_METHOD("stop_lock_free_test"),
                           &LockFreeExample::stop_lock_free_test);
        ClassDB::bind_method(D_METHOD("send_message", "message"),
                           &LockFreeExample::send_message);
    }

public:
    ~LockFreeExample() {
        stop_lock_free_test();
    }

    void start_lock_free_test() {
        if (running.load()) return;

        running.store(true);

        // Start producer thread
        producer_thread = std::thread([this]() {
            int counter = 0;
            while (running.load()) {
                String message = "Auto message " + itos(counter++);

                if (!message_queue.try_enqueue(message)) {
                    // Queue full, wait a bit
                    std::this_thread::sleep_for(std::chrono::milliseconds(1));
                } else {
                    std::this_thread::sleep_for(std::chrono::milliseconds(10));
                }
            }
        });

        // Start consumer thread
        consumer_thread = std::thread([this]() {
            String message;
            while (running.load()) {
                if (message_queue.try_dequeue(message)) {
                    // Process message (simulate work)
                    std::this_thread::sleep_for(std::chrono::milliseconds(5));

                    // Queue result for main thread
                    queue_main_thread_print(message);
                } else {
                    std::this_thread::yield();
                }
            }
        });
    }

    void stop_lock_free_test() {
        running.store(false);

        if (producer_thread.joinable()) {
            producer_thread.join();
        }

        if (consumer_thread.joinable()) {
            consumer_thread.join();
        }
    }

    void send_message(const String& message) {
        if (!message_queue.try_enqueue(message)) {
            WARN_PRINT("Message queue full, dropping message: " + message);
        }
    }

    void _process(double delta) override {
        // Process queued print messages on main thread
        process_main_thread_prints();
    }

private:
    LockFreeQueue<String, 256> print_queue;

    void queue_main_thread_print(const String& message) {
        print_queue.try_enqueue("Processed: " + message);
    }

    void process_main_thread_prints() {
        String message;
        int processed = 0;

        // Limit processing to avoid frame drops
        while (processed < 10 && print_queue.try_dequeue(message)) {
            UtilityFunctions::print(message);
            processed++;
        }
    }
};

Thread-Safe Singletons

Thread-Safe Singleton Pattern

class ThreadSafeSingleton : public RefCounted {
    GDCLASS(ThreadSafeSingleton, RefCounted)

private:
    static std::once_flag initialized_flag;
    static ThreadSafeSingleton* instance;
    static std::mutex instance_mutex;

    // Thread-safe data
    std::atomic<int> request_count{0};
    mutable std::shared_mutex data_mutex;
    HashMap<String, Variant> shared_data;

protected:
    static void _bind_methods() {
        ClassDB::bind_static_method("ThreadSafeSingleton",
                                   D_METHOD("get_instance"),
                                   &ThreadSafeSingleton::get_instance);
        ClassDB::bind_method(D_METHOD("increment_requests"),
                           &ThreadSafeSingleton::increment_requests);
        ClassDB::bind_method(D_METHOD("get_request_count"),
                           &ThreadSafeSingleton::get_request_count);
        ClassDB::bind_method(D_METHOD("set_data", "key", "value"),
                           &ThreadSafeSingleton::set_data);
        ClassDB::bind_method(D_METHOD("get_data", "key"),
                           &ThreadSafeSingleton::get_data);
    }

    ThreadSafeSingleton() {
        // Private constructor
        UtilityFunctions::print("ThreadSafeSingleton instance created");
    }

public:
    static ThreadSafeSingleton* get_instance() {
        // Thread-safe lazy initialization using std::call_once
        std::call_once(initialized_flag, []() {
            instance = memnew(ThreadSafeSingleton);
        });
        return instance;
    }

    void increment_requests() {
        request_count.fetch_add(1, std::memory_order_relaxed);
    }

    int get_request_count() const {
        return request_count.load(std::memory_order_relaxed);
    }

    void set_data(const String& key, const Variant& value) {
        std::unique_lock<std::shared_mutex> lock(data_mutex);
        shared_data[key] = value;
    }

    Variant get_data(const String& key) const {
        std::shared_lock<std::shared_mutex> lock(data_mutex);

        if (shared_data.has(key)) {
            return shared_data[key];
        }
        return Variant();
    }

    // Bulk operations for better performance
    void set_data_batch(const Dictionary& data) {
        std::unique_lock<std::shared_mutex> lock(data_mutex);

        for (const KeyValue<Variant, Variant>& E : data) {
            shared_data[E.key] = E.value;
        }
    }

    Dictionary get_all_data() const {
        std::shared_lock<std::shared_mutex> lock(data_mutex);

        Dictionary result;
        for (const KeyValue<String, Variant>& E : shared_data) {
            result[E.key] = E.value;
        }
        return result;
    }

    static void cleanup() {
        std::lock_guard<std::mutex> lock(instance_mutex);
        if (instance) {
            memdelete(instance);
            instance = nullptr;
        }
    }
};

// Static member definitions
std::once_flag ThreadSafeSingleton::initialized_flag;
ThreadSafeSingleton* ThreadSafeSingleton::instance = nullptr;
std::mutex ThreadSafeSingleton::instance_mutex;

Deadlock Prevention

Deadlock Prevention Strategies

class DeadlockPrevention : public RefCounted {
    GDCLASS(DeadlockPrevention, RefCounted)

private:
    std::mutex resource_a_mutex;
    std::mutex resource_b_mutex;
    std::mutex resource_c_mutex;

    int resource_a = 100;
    int resource_b = 200;
    int resource_c = 300;

protected:
    static void _bind_methods() {
        ClassDB::bind_method(D_METHOD("transfer_a_to_b", "amount"),
                           &DeadlockPrevention::transfer_a_to_b);
        ClassDB::bind_method(D_METHOD("transfer_b_to_a", "amount"),
                           &DeadlockPrevention::transfer_b_to_a);
        ClassDB::bind_method(D_METHOD("three_way_transfer", "amount"),
                           &DeadlockPrevention::three_way_transfer);
    }

public:
    // BAD: Inconsistent lock ordering can cause deadlocks
    void transfer_a_to_b_unsafe(int amount) {
        std::lock_guard<std::mutex> lock_a(resource_a_mutex);  // A first
        std::lock_guard<std::mutex> lock_b(resource_b_mutex);  // B second

        resource_a -= amount;
        resource_b += amount;
    }

    void transfer_b_to_a_unsafe(int amount) {
        std::lock_guard<std::mutex> lock_b(resource_b_mutex);  // B first - DEADLOCK!
        std::lock_guard<std::mutex> lock_a(resource_a_mutex);  // A second

        resource_b -= amount;
        resource_a += amount;
    }

    // GOOD: Consistent lock ordering prevents deadlocks
    void transfer_a_to_b(int amount) {
        // Always lock in the same order: A before B
        std::lock_guard<std::mutex> lock_a(resource_a_mutex);
        std::lock_guard<std::mutex> lock_b(resource_b_mutex);

        resource_a -= amount;
        resource_b += amount;
    }

    void transfer_b_to_a(int amount) {
        // Same lock order: A before B
        std::lock_guard<std::mutex> lock_a(resource_a_mutex);
        std::lock_guard<std::mutex> lock_b(resource_b_mutex);

        resource_b -= amount;
        resource_a += amount;
    }

    // BETTER: Use std::lock for atomic multi-mutex acquisition
    void three_way_transfer(int amount) {
        // Acquire all three mutexes atomically
        std::lock(resource_a_mutex, resource_b_mutex, resource_c_mutex);

        // Use adopt_lock since mutexes are already locked
        std::lock_guard<std::mutex> lock_a(resource_a_mutex, std::adopt_lock);
        std::lock_guard<std::mutex> lock_b(resource_b_mutex, std::adopt_lock);
        std::lock_guard<std::mutex> lock_c(resource_c_mutex, std::adopt_lock);

        // Perform three-way transfer
        resource_a -= amount;
        resource_b += amount / 2;
        resource_c += amount / 2;
    }

    // Timeout-based deadlock avoidance
    bool try_transfer_with_timeout(int amount, std::chrono::milliseconds timeout) {
        std::unique_lock<std::mutex> lock_a(resource_a_mutex, std::defer_lock);
        std::unique_lock<std::mutex> lock_b(resource_b_mutex, std::defer_lock);

        // Try to lock both within timeout
        if (std::try_lock(lock_a, lock_b) == -1) {
            // Success - both locks acquired
            resource_a -= amount;
            resource_b += amount;
            return true;
        } else {
            // Failed to acquire both locks
            return false;
        }
    }

    // Hierarchical locking (by address)
    void hierarchical_transfer(DeadlockPrevention* other, int amount) {
        if (!other) return;

        // Lock mutexes in address order to prevent deadlocks
        std::mutex* first_mutex;
        std::mutex* second_mutex;

        if (this < other) {
            first_mutex = &resource_a_mutex;
            second_mutex = &other->resource_a_mutex;
        } else {
            first_mutex = &other->resource_a_mutex;
            second_mutex = &resource_a_mutex;
        }

        std::lock_guard<std::mutex> lock1(*first_mutex);
        std::lock_guard<std::mutex> lock2(*second_mutex);

        // Safe to transfer between objects
        resource_a -= amount;
        other->resource_a += amount;
    }

    Dictionary get_resources() const {
        // Use std::lock for reading multiple resources atomically
        std::lock(resource_a_mutex, resource_b_mutex, resource_c_mutex);
        std::lock_guard<std::mutex> lock_a(resource_a_mutex, std::adopt_lock);
        std::lock_guard<std::mutex> lock_b(resource_b_mutex, std::adopt_lock);
        std::lock_guard<std::mutex> lock_c(resource_c_mutex, std::adopt_lock);

        Dictionary result;
        result["a"] = resource_a;
        result["b"] = resource_b;
        result["c"] = resource_c;
        return result;
    }
};

Testing Thread Safety

Thread Safety Testing Framework

class ThreadSafetyTester : public RefCounted {
    GDCLASS(ThreadSafetyTester, RefCounted)

private:
    std::atomic<int> test_counter{0};
    std::atomic<bool> test_failed{false};
    std::atomic<int> active_threads{0};

    Vector<std::thread> test_threads;

protected:
    static void _bind_methods() {
        ClassDB::bind_method(D_METHOD("run_concurrency_test", "thread_count", "iterations"),
                           &ThreadSafetyTester::run_concurrency_test);
        ClassDB::bind_method(D_METHOD("run_atomic_test"), &ThreadSafetyTester::run_atomic_test);
        ClassDB::bind_method(D_METHOD("run_mutex_test"), &ThreadSafetyTester::run_mutex_test);
    }

public:
    Dictionary run_concurrency_test(int thread_count, int iterations) {
        test_counter.store(0);
        test_failed.store(false);
        active_threads.store(0);

        auto start_time = std::chrono::high_resolution_clock::now();

        // Start test threads
        test_threads.clear();
        test_threads.reserve(thread_count);

        for (int i = 0; i < thread_count; i++) {
            test_threads.emplace_back([this, iterations]() {
                active_threads.fetch_add(1);

                for (int j = 0; j < iterations; j++) {
                    // Test atomic increment
                    int old_value = test_counter.fetch_add(1);

                    // Verify no race condition occurred
                    if (old_value < 0) {
                        test_failed.store(true);
                        break;
                    }

                    // Simulate some work
                    std::this_thread::yield();
                }

                active_threads.fetch_sub(1);
            });
        }

        // Wait for all threads to complete
        for (auto& thread : test_threads) {
            thread.join();
        }

        auto end_time = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);

        Dictionary result;
        result["expected_count"] = thread_count * iterations;
        result["actual_count"] = test_counter.load();
        result["test_passed"] = !test_failed.load() && (test_counter.load() == thread_count * iterations);
        result["duration_microseconds"] = static_cast<int>(duration.count());
        result["operations_per_second"] = static_cast<int>((thread_count * iterations * 1000000.0) / duration.count());

        return result;
    }

    Dictionary run_atomic_test() {
        const int num_threads = 8;
        const int iterations = 100000;

        std::atomic<int> atomic_counter{0};
        int unsafe_counter = 0;
        std::mutex unsafe_mutex;

        Vector<std::thread> threads;
        auto start_time = std::chrono::high_resolution_clock::now();

        // Test atomic operations
        for (int i = 0; i < num_threads; i++) {
            threads.emplace_back([&atomic_counter, &unsafe_counter, &unsafe_mutex, iterations]() {
                for (int j = 0; j < iterations; j++) {
                    // Atomic increment (thread-safe)
                    atomic_counter.fetch_add(1, std::memory_order_relaxed);

                    // Unsafe increment for comparison
                    {
                        std::lock_guard<std::mutex> lock(unsafe_mutex);
                        unsafe_counter++;
                    }
                }
            });
        }

        for (auto& thread : threads) {
            thread.join();
        }

        auto end_time = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);

        Dictionary result;
        result["atomic_result"] = atomic_counter.load();
        result["mutex_result"] = unsafe_counter;
        result["expected_result"] = num_threads * iterations;
        result["atomic_correct"] = (atomic_counter.load() == num_threads * iterations);
        result["mutex_correct"] = (unsafe_counter == num_threads * iterations);
        result["duration_microseconds"] = static_cast<int>(duration.count());

        return result;
    }

    Dictionary run_mutex_test() {
        const int num_threads = 4;
        const int iterations = 50000;

        std::mutex test_mutex;
        int shared_counter = 0;
        bool data_corruption = false;

        Vector<std::thread> threads;
        auto start_time = std::chrono::high_resolution_clock::now();

        for (int i = 0; i < num_threads; i++) {
            threads.emplace_back([&test_mutex, &shared_counter, &data_corruption, iterations]() {
                for (int j = 0; j < iterations; j++) {
                    std::lock_guard<std::mutex> lock(test_mutex);

                    int old_value = shared_counter;
                    std::this_thread::yield();  // Increase chance of race condition without mutex
                    shared_counter = old_value + 1;

                    // Verify integrity
                    if (shared_counter <= old_value) {
                        data_corruption = true;
                    }
                }
            });
        }

        for (auto& thread : threads) {
            thread.join();
        }

        auto end_time = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);

        Dictionary result;
        result["final_count"] = shared_counter;
        result["expected_count"] = num_threads * iterations;
        result["test_passed"] = !data_corruption && (shared_counter == num_threads * iterations);
        result["data_corruption_detected"] = data_corruption;
        result["duration_microseconds"] = static_cast<int>(duration.count());

        return result;
    }
};

Best Practices Summary

1. Thread Context Awareness

2. Synchronization Strategy

3. Deadlock Prevention

4. Testing and Validation

Conclusion

Thread safety in GDExtension requires:

Proper thread safety ensures stable, reliable extensions that can leverage multi-core performance without risking data corruption or crashes.