R&D · UE5 · Concurrency · C++

Multithreading Sandbox

Multithreading Sandbox

A deep dive into every threading layer Unreal Engine exposes. Core-pinned FRunnable workers eliminate cross-core cache invalidation; an MPSC lock-free task queue handles cross-thread dispatch; a DAG task graph fires tasks only when all upstream dependencies complete. Game-thread result marshalling and a background prime-sieve demo round out the coverage.

FRunnable MPSC Queue DAG Task Graph Core Affinity UE5 C++
What I Built
  • Core-pinned FRunnable workers — 1 core per thread (RunnableGoon) — each thread affinity-masked to 1 dedicated CPU core via SetThreadAffinityMask, eliminating cross-core cache invalidation with 0 lock contention on affinity.
  • MPSC lock-free task queue — 1 shared queue1 TQueue<TSharedPtr<ITask>, EQueueMode::Mpsc> with 1 scoped critical section per drain cycle for safe cross-thread task dispatch.
  • DAG task graph — N-node dependency tracking (TaskGraphManager) — arbitrary dependency chains; each node fires its lambda enqueue only when all upstream dependencies signal completion, supporting 0 premature executions.
  • Game-thread position sync — 1 async marshal (UpdatePositionsTask) — background threads compute transform data; 1 async task per batch marshals results back to the game thread for actor updates.
  • Background prime sieve — 1 thread-safe result queue (PrimeCalculationTask) — heavyweight CPU sieve offloaded to worker threads with 1 thread-safe dequeue for result delivery to the game thread.

Core-Pinned Worker — RunnableGoon

Each worker is pinned to a specific CPU core on startup. Tasks arrive via MPSC queue; ExecuteTasks() drains the queue under a single scoped lock, keeping contention windows minimal.

RunnableGoon.h — core-pinned FRunnable + MPSC task queue
class RunnableGoon : public FRunnable
{
public:
    RunnableGoon(EThreadPriority InPriority,
                 TQueue<TSharedPtr<ITask>, EQueueMode::Mpsc>& InTaskQueue,
                 FCriticalSection& InCritSec,
                 int32 CoreAffinity);

    virtual uint32 Run() override
    {
        // Pin to specific CPU core — eliminates cross-core cache invalidation
        HANDLE hThread = GetCurrentThread();
        SetThreadAffinityMask(hThread, static_cast<DWORD_PTR>(1ULL << CoreAffinity));

        while (StopTaskCounter.GetValue() == 0)
            ExecuteTasks();

        return 0;
    }

    virtual void Stop() override { StopTaskCounter.Increment(); }

private:
    FThreadSafeCounter  StopTaskCounter;
    TQueue<TSharedPtr<ITask>, EQueueMode::Mpsc>& TaskQueue;
    FCriticalSection&   TaskQueueCritSec;
    int32               CoreAffinity;

    void ExecuteTasks()
    {
        FScopeLock Lock(&TaskQueueCritSec);
        TSharedPtr<ITask> Task;
        while (TaskQueue.Dequeue(Task))
            if (Task.IsValid()) Task->Execute();
    }
};
Dependency-Graph Task Scheduler

TaskGraphManager tracks an arbitrary DAG via FTaskNode. When all of a node's upstream dependencies complete, OnReadyToExecute fires and enqueues it into the thread pool.

TaskGraphManager.cpp — AddTask() + Execute()
class TaskGraphManager
{
public:
    void AddTask(TSharedPtr<FTaskNode> Node)
    {
        FScopeLock Lock(&TaskCritSec);
        NewTaskNodes.Add(Node);
        // Lambda fires when all upstream deps are done
        Node->OnReadyToExecute.AddLambda([this, Node]()
        {
            PoolManager->AddTask(Node->GetTask());
        });
    }

    void Execute()
    {
        {
            FScopeLock Lock(&TaskCritSec);
            if (bIsExecuting) return;
            bIsExecuting = true;
            ExecutingTaskNodes.Append(NewTaskNodes);
            NewTaskNodes.Empty();
        }
        // Kick off any node whose dependencies are already satisfied
        for (auto& Node : ExecutingTaskNodes)
            if (Node->AreDependenciesCompleted())
                Node->MarkReady();   // triggers OnReadyToExecute → enqueue

        { FScopeLock Lock(&TaskCritSec); bIsExecuting = false; }
    }

private:
    TArray<TSharedPtr<FTaskNode>> ExecutingTaskNodes;
    TArray<TSharedPtr<FTaskNode>> NewTaskNodes;
    ThreadPoolManager*             PoolManager;
    mutable FCriticalSection        TaskCritSec;
    bool                           bIsExecuting = false;
};
Engine Unreal Engine 5 Language C++ Threading FRunnable · core-pinned workers Queue MPSC lock-free TQueue Scheduling DAG dependency graph Category R&D · Performance · Systems Source github.com/khaled71612000 ↗
Connect