$npx -y skills add quodsoler/unreal-engine-skills --skill ue-async-threadingUse this skill when working with Unreal Engine async operations, threading, parallel execution, or concurrency. Also use when the user mentions 'FRunnable', 'FAsyncTask', 'TaskGraph', 'UE::Tasks', 'ParallelFor', 'TFuture', 'TPromise', 'Async()', 'thread safety', 'FCriticalSection
| 1 | # UE Async and Threading |
| 2 | |
| 3 | You are an expert in Unreal Engine's threading model, async task systems, and concurrent programming patterns. |
| 4 | |
| 5 | ## Context Check |
| 6 | |
| 7 | Read `.agents/ue-project-context.md` before proceeding. Engine version matters: `UE::Tasks::Launch` is the modern preferred API (UE 5.0+), while `FAsyncTask` and TaskGraph remain fully supported. Determine: What work needs to be offloaded? Is UObject access required? What latency/throughput tradeoff is acceptable? |
| 8 | |
| 9 | ## Information Gathering |
| 10 | |
| 11 | Ask the user if unclear: |
| 12 | - **Offload type** — CPU-bound computation, I/O wait, or periodic background work? |
| 13 | - **UObject interaction** — Does the background work need to read/write UObject state? |
| 14 | - **Lifetime** — One-shot task, recurring work, or long-lived thread? |
| 15 | - **Result delivery** — Fire-and-forget, or does the game thread need results back? |
| 16 | |
| 17 | --- |
| 18 | |
| 19 | ## UE Threading Model |
| 20 | |
| 21 | UE runs several named threads plus a scalable worker pool. Understanding which thread owns what prevents the most common threading bugs. |
| 22 | |
| 23 | **Named threads:** |
| 24 | - **Game Thread** — All UObject access, Blueprint execution, gameplay logic. Check with `IsInGameThread()`. |
| 25 | - **Render Thread** — Render commands, scene proxy updates. `IsInRenderingThread()`. |
| 26 | - **RHI Thread** — GPU command submission (platform-dependent). |
| 27 | - **Worker Threads** — Unnamed pool threads for task dispatch. Count scales with CPU cores. |
| 28 | |
| 29 | **The golden rule:** UObjects are game-thread-only. No UPROPERTY reads, no UFUNCTION calls, no `GetWorld()`, no spawning from background threads. Violating this causes intermittent crashes that depend on GC timing and are extremely difficult to diagnose. |
| 30 | |
| 31 | --- |
| 32 | |
| 33 | ## Pattern Selection Guide |
| 34 | |
| 35 | Choose the simplest API that fits your needs. |
| 36 | |
| 37 | | Pattern | Best For | Lifetime | Result? | |
| 38 | |---------|----------|----------|---------| |
| 39 | | `AsyncTask(GameThread, Lambda)` | Dispatch to game thread from background | One-shot | No | |
| 40 | | `UE::Tasks::Launch` | General async work (preferred, UE5+) | One-shot | `TTask<T>` | |
| 41 | | `Async(EAsyncExecution, Lambda)` | Flexible dispatch with `TFuture` | One-shot | `TFuture<T>` | |
| 42 | | `FAsyncTask<T>` | Reusable pooled work units | Reusable | Via `GetTask()` | |
| 43 | | `FAutoDeleteAsyncTask<T>` | Fire-and-forget pooled work | One-shot | No | |
| 44 | | `TGraphTask<T>` | Complex dependency graphs | One-shot | `FGraphEvent` | |
| 45 | | `ParallelFor` | Data-parallel loops | Blocking | No | |
| 46 | | `FRunnable` + `FRunnableThread` | Long-lived dedicated threads | Persistent | Manual | |
| 47 | |
| 48 | --- |
| 49 | |
| 50 | ## FRunnable and FRunnableThread |
| 51 | |
| 52 | Use `FRunnable` only when you need a **dedicated, long-lived thread** -- a socket listener, a file watcher, or a continuous processing loop. For one-shot work, prefer `UE::Tasks::Launch` or `FAsyncTask`. |
| 53 | |
| 54 | **Lifecycle:** `Init()` (new thread) -> `Run()` (new thread) -> `Exit()` (new thread, after Run returns). `Stop()` is called externally to request shutdown. |
| 55 | |
| 56 | **FRunnableThread::Create** signature: `static FRunnableThread* Create(FRunnable*, const TCHAR* ThreadName, uint32 StackSize = 0, EThreadPriority = TPri_Normal, uint64 AffinityMask, EThreadCreateFlags)`. |
| 57 | |
| 58 | **Key points:** `Stop()` signals the thread -- it does not block. `Kill(true)` calls `Stop()` then waits for completion. Always `delete` the `FRunnableThread*` after `Kill`. Use `std::atomic<bool> bShouldStop` in `Run()` loop, set it in `Stop()`. |
| 59 | |
| 60 | See `references/threading-patterns.md` for a complete `FRunnable` subclass template with proper shutdown. |
| 61 | |
| 62 | --- |
| 63 | |
| 64 | ## FAsyncTask and FAutoDeleteAsyncTask |
| 65 | |
| 66 | For **reusable work units** on the engine thread pool (`GThreadPool`). Subclass `FNonAbandonableTask` and implement `DoWork()` + `GetStatId()`. |
| 67 | |
| 68 | ```cpp |
| 69 | class FMyComputeTask : public FNonAbandonableTask |
| 70 | { |
| 71 | friend class FAsyncTask<FMyComputeTask>; |
| 72 | int32 Result = 0; |
| 73 | TArray<int32> InputData; |
| 74 | |
| 75 | FMyComputeTask(TArray<int32> InData) : InputData(MoveTemp(InData)) {} |
| 76 | |
| 77 | void DoWork() |
| 78 | { |
| 79 | for (int32 Val : InputData) { Result += Val; } |
| 80 | } |
| 81 | |
| 82 | FORCEINLINE TStatId GetStatId() const |
| 83 | { |
| 84 | RETURN_QUICK_DECLARE_CYCLE_STAT(FMyComputeTask, STATGROUP_ThreadPoolAsyncTasks); |
| 85 | } |
| 86 | }; |
| 87 | ``` |
| 88 | |
| 89 | **Usage:** |
| 90 | |
| 91 | ```cpp |
| 92 | // Reusable — you manage lifetime |
| 93 | auto* Task = new FAsyncTask<FMyComputeTask>(MoveTemp(Data)); |
| 94 | Task->StartBackgroundTask(); // dispatches to GThreadPool |
| 95 | Task->EnsureCompletion(); // blocks or runs inline if not started |
| 96 | int32 R = Task->GetTask().Result; |
| 97 | delete Task; |
| 98 | |
| 99 | // Fire-and-forget — auto-deletes on completion |
| 100 | (new FAut |