ue-async-threading

Use 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', 'FRWLock', 'backg

By quodsoler · 792 installs

npx skills add quodsoler/unreal-engine-skills --skill ue-async-threading

Source repository · Upstream listing

UE Async and Threading Target engine: UE 5.8 . APIs below are verified against the 5.8 headers; older forms are listed under "Deprecated — do not use". This skill covers moving work off the game thread and bringing results back: the UE::Tasks system ( Tasks/Task.h , Tasks/Pipe.h ), Async / TFuture ( Async/Async.h , Async/Future.h ), thread pool FAsyncTask ( Async/AsyncWork.h ), ParallelFor , dedicated FRunnable threads, locks and lock free queues, FTSTicker and FTimerManager . Everything except timers is in the Core module; FTimerManager is in Engine ; ENQUEUE RENDER COMMAND is in RenderCore . Build.cs: PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" }); and add "RenderCore" only when enqueuing render commands. Context Read .agents/ue project context.md if it exists (module names, conventions, enabled plugins, GAS/networking setup). Do not stop if it is missing. Identify the area from the request and the codebase. Ask only when two plausible readings would produce different code. Request is about… Go to Which API fits [Pattern Selection]( pattern selection) One shot background work, chaining, prerequisites, events [UE::Tasks]( uetasks) Serializing access to a resource without a dedicated thread [FPipe and FTaskConcurrencyLimiter]( fpipe and ftaskconcurrencylimiter) Futures, promises, dispatch to the game thread [Async, TFuture and AsyncTask]( async tfuture and asynctask) Reusable pooled work unit [FAsyncTask and FAutoDeleteAsyncTask]( fasynctask and fautodeleteasynctask) Data parallel loops [ParallelFor]( parallelfor) Long lived dedicated thread [FRunnable and FRunnableThread]( frunnable and frunnablethread) Locks, events, atomics, queues, shared pointers [Synchronization]( synchronization) Per frame callbacks and delayed calls [Tickers and Timers]( tickers and timers) UObject, GC and render thread rules [Thread Safety Rules]( thread safety rules) Old forms [Deprecated — do not use]( deprecated do not use) Threading Model Thread Check Owns Game thread IsInGameThread() ( CoreGlobals.h ) All UObject access, Blueprint, gameplay, timers, tickers Render thread IsInRenderingThread() Scene proxies, render commands ( ENQUEUE RENDER COMMAND ) RHI thread IsInRHIThread() GPU command submission Worker threads none UE::Tasks scheduler, TaskGraph AnyThread , ParallelFor Thread pools none GThreadPool , GBackgroundPriorityThreadPool , GIOThreadPool ; GLargeThreadPool only WITH EDITOR ( Misc/QueuedThreadPool.h ) Golden rule: UObjects are game thread only. Compute off thread on plain data, then apply results on the game thread through a TWeakObjectPtr (see [Thread Safety Rules]( thread safety rules)). Pattern Selection Need Use Result One shot background work, dependencies, chaining UE::Tasks::Launch TTask<T Run a lambda on the game thread from anywhere AsyncTask(ENamedThreads::GameThread, ...) none Future style result with execution context choice Async(EAsyncExecution, ...) TFuture<T Serialize tasks touching one resource (FIFO) UE::Tasks::FPipe TTask<T Cap how many tasks run at once UE::Tasks::FTaskConcurrencyLimiter none Reusable pooled work unit with owner managed lifetime FAsyncTask<T / FAutoDeleteAsyncTask<T via GetTask() Data parallel loop, caller blocks ParallelFor none Long lived thread (socket, file watcher, sim loop) FRunnable + FRunnableThread::Create manual Per frame callback outside an Actor tick FTSTicker::GetCoreTicker().AddTicker handle Delayed or repeating call on the game thread FTimerManager::SetTimer FTimerHandle UE::Tasks Handle API ( Tasks/Task.h:36 95 ): IsValid() , IsCompleted() , Wait() , Wait(FTimespan Timeout) returns bool , TryRetractAndExecute() runs the task inline if not started, GetResult() waits then returns ResultType& ( check(IsValid()) ). Free functions: Wait(FTask&) , Wait(Collection, FTimespan) , WaitAny(Collection, Timeout) returns the index, Any(Collection) returns an FTask completed when any input completes, AddNested(Task) from inside a running task so the parent is not complete until the nested one is. Priorities ( Async/Fundamental/Task.h:20 , Tasks/TaskPrivate.h:59 93 ): ETaskPriority::High Normal (= Default) BackgroundHigh BackgroundNormal BackgroundLow Inherit . EExtendedTaskPriority::None Inline TaskEvent GameThreadNormalPri GameThreadHiPri GameThreadNormalPriLocalQueue GameThreadHiPriLocalQueue RenderThreadNormalPri RenderThreadHiPri RHIThreadNormalPri RHIThreadHiPri (plus LocalQueue variants). ETaskFlags::None DoNotRunInsideBusyWait . Passing EExtendedTaskPriority::GameThreadNormalPri runs the body on the game thread. A legacy TaskGraph FGraphEventRef can be passed directly as the prerequisites argument of Launch ( Tasks/TaskPrivate.h:266 ), and collections of them work in WaitAny / Any ( Tasks/Task.h:417, 468 ), so UE::Tasks can wait on TaskGraph work. Full chained example: [threading patterns.md](references/threading patterns.md uetasks chain with ftaskevent and cancellation). FPipe and FTaskConcurrencyLimiter FPipe executes its tasks one after another (FIFO when no extra prerequisites), so it replaces a dedicated thread guarding a resource. The pipe must outlive its last task; ~FPipe() asserts !HasWork() . FPipe::Launch(const TCHAR , TaskBody, [Prerequisites,] ETaskPriority = Default, EExtendedTaskPriority = None, ETaskFlags = None) ( Tasks/Pipe.h:63,90 ). FTaskConcurrencyLimiter may be destroyed before its tasks finish; its Wait is satisfied once and never re arms ( Tasks/TaskConcurrencyLimiter.h:171 212 ). Async, TFuture and AsyncTask EAsyncExecution ( Async/Async.h:27 ) Runs on TaskGraph Worker thread, short tasks TaskGraphMainThread Game thread, may run inside GC or PostLoad waits — only for code safe anywhere TaskGraphMainTick Game thread inside a Tick — the safe choice for delegates and UObject code Thread New dedicated thread, long running or blocking I/O ThreadIfForkSafe As Thread , fork aware ThreadPool GThreadPool LargeThreadPool GLargeThreadPool , WITH EDITOR only TFuture<T ( Async/Future.h:210 440 ): Get() , IsReady() , IsValid() , Wait() , WaitFor(const FTimespan&) , WaitUntil(const FDateTime&) , Then(Func) receives TFuture<T , Next(Func) receives T (both move the state out and invalidate this future, Async/Future.h:669 ), Consume() moves the value out and invalidates, Share() gives TSharedFuture<T , Reset() . TPromise<T ( :527 ): GetFuture() , SetValue(const T&) , SetValue(T&&) , EmplaceValue(Args&&...) . Continuations run on whichever thread completes the promise — hop to the game thread explicitly. FAsyncTask and FAutoDeleteAsyncTask Reusable work unit on a FQueuedThreadPool . Subclass FNonAbandonableTask ( Async/AsyncWork.h:666 ), implement DoWork() and GetStatId() . FAsyncTask<T constructs T inside its own constructor, so with the friend declaration T 's constructor may be private (the engine's own example in AsyncWork.h:26 42 does this). Members your code reads through Task GetTask() must be public , because that access happens outside the friend. Other members ( Async/AsyncWork.h:415 558 ): StartSynchronousTask(...) runs inline; Cancel() returns true if it was still queued; WaitCompletionWithTimeout(float TimeLimitSeconds) ; IsWorkDone() is the cheap non blocking check. Pass GBackgroundPriorityThreadPool as the pool for low priority work. Full template: [threading patterns.md](references/threading patterns.md fnonabandonabletask fasynctask template). ParallelFor EParallelForFlags ( Async/ParallelFor.h:46 ) Effect None Default ForceSingleThread Run sequentially on the caller (debugging) Unbalanced Iterations have very different costs; smaller batches PumpRenderingThread Caller pumps render commands while waiting BackgroundPriority Workers run at background priority Also available: ParallelForTemplate(...) (no TFunctionRef indirection, :496 ), ParallelForWithPreWork(...) ( :571 , run caller side work before helping), ParallelForWithTaskContext(OutContexts, Num, ContextConstructor, Body, Flags) ( :722 ), ParallelForWithExistingTaskContext(TArrayView<ContextType Contexts, Num, MinBatchSize, Body, Flags) ( :815 ). The caller participates and blocks until every iteration finishes. CVar Async.ParallelFor.DisableOversubscription ( GParallelForDisableOversubscription , Async/ParallelFor.h:43 ) stops ParallelFor from waking extra workers. FRunnable and FRunnableThread Use only for a dedicated, long lived thread. Lifecycle on the new thread: Init() → Run() → Exit() . Stop() is called from outside by Kill() ; it must only signal. EThreadPriority ( GenericPlatform/GenericPlatformAffinity.h:25 ): TPri Normal, TPri AboveNormal, TPri BelowNormal, TPri Highest, TPri Lowest, TPri SlightlyBelowNormal, TPri TimeCritical . Always Kill(true) then delete the FRunnableThread ; killing without waiting leaks and can deadlock (header comment HAL/RunnableThread.h:77 79 ). Sleep with FPlatformProcess::Sleep(float Seconds) or block on an FEventRef instead of spinning. Full template with shutdown: [threading patterns.md](references/threading patterns.md frunnable subclass template). Synchronization Need Type RAII guard Header General mutex, recursive FCriticalSection (= UE::FPlatformRecursiveMutex ) FScopeLock Lock(&Mutex) ; FScopeUnlock to release inside a scope HAL/CriticalSection.h:53 , Misc/ScopeLock.h:140 Many readers, one writer, not recursive FRWLock (= UE::FPlatformRWLock ) FReadScopeLock(FRWLock&) , FWriteScopeLock(FRWLock&) , FRWScopeLock(Lock, SLT ReadOnly / SLT Write) HAL/CriticalSection.h:56 , Misc/ScopeRWLock.h:92 198 One byte, non recursive, unfair, fastest UE::FMutex UE::TUniqueLock<UE::FMutex ; UE::TDynamicUniqueLock with UE::DeferLock Async/Mutex.h:18 , Async/UniqueLock.h:19,48 , Async/LockTags.h:12 Small recursive mutex UE::FRecursiveMutex UE::TUniqueLock Async/RecursiveMutex.h:19 Small readers/writer UE::FSharedMutex ( LockShared/UnlockShared ) UE::TSharedLock , UE::TUniqueLock Async/SharedMutex.h:22 , Async/SharedLock.h:21 Guard any type with Lock()/Unlock() UE::TScopeLock<MutexType — Misc/ScopeLock.h:25 Events: FEventRef Event(EEventMode::AutoReset) ( HAL/Event.h:129 139 ) is the RAII pooled FEvent : Event Trigger() , Event Wait() , Event Wait(uint32 WaitTimeMs) , Event Reset() . Raw pooling: FEvent E = FPlatformProcess::GetSynchEventFromPool(bool bIsManualReset = false) / FPlatformProcess::ReturnSynchEventToPool(E) ( GenericPlatformProcess.h:786,799 ). UE::FManualResetEvent ( Async/ManualResetEvent.h ): Notify() , Wait() , WaitFor(FMonotonicTimeSpan) , Reset() . Between tasks prefer UE::Tasks::FTaskEvent — waiting on it does not block a worker. Atomics: std::atomic<T ( <atomic , already included by Templates/Atomic.h ). FThreadSafeCounter , FThreadSafeBool and TAtomic are marked deprecated in their headers (see [Deprecated]( deprecated do not use)). Use std::memory order relaxed for pure flags and counters, acquire / release when the atomic publishes other data. Queues: TMpscQueue<T ( Containers/MpscQueue.h ) and TSpscQueue<T ( Containers/SpscQueue.h ): Enqueue(Args&&...) , bool Dequeue(T& Out) , TOptional<T Dequeue() , T Peek() , IsEmpty() . Single consumer only. TQueue<T, EQueueMode still compiles but is marked "planned for deprecation" ( Co