Loading...
Loading...
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', 'background thread', 'game thread dispatch', or 'thread pool'. For networking async (RPCs, replication), see ue-networking-replication. For asset streaming, see ue-data-assets-tables.
npx skill4agent add quodsoler/unreal-engine-skills ue-async-threading.agents/ue-project-context.mdUE::Tasks::LaunchFAsyncTaskIsInGameThread()IsInRenderingThread()GetWorld()| Pattern | Best For | Lifetime | Result? |
|---|---|---|---|
| Dispatch to game thread from background | One-shot | No |
| General async work (preferred, UE5+) | One-shot | |
| Flexible dispatch with | One-shot | |
| Reusable pooled work units | Reusable | Via |
| Fire-and-forget pooled work | One-shot | No |
| Complex dependency graphs | One-shot | |
| Data-parallel loops | Blocking | No |
| Long-lived dedicated threads | Persistent | Manual |
FRunnableUE::Tasks::LaunchFAsyncTaskInit()Run()Exit()Stop()static FRunnableThread* Create(FRunnable*, const TCHAR* ThreadName, uint32 StackSize = 0, EThreadPriority = TPri_Normal, uint64 AffinityMask, EThreadCreateFlags)Stop()Kill(true)Stop()deleteFRunnableThread*Killstd::atomic<bool> bShouldStopRun()Stop()references/threading-patterns.mdFRunnableGThreadPoolFNonAbandonableTaskDoWork()GetStatId()class FMyComputeTask : public FNonAbandonableTask
{
friend class FAsyncTask<FMyComputeTask>;
int32 Result = 0;
TArray<int32> InputData;
FMyComputeTask(TArray<int32> InData) : InputData(MoveTemp(InData)) {}
void DoWork()
{
for (int32 Val : InputData) { Result += Val; }
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FMyComputeTask, STATGROUP_ThreadPoolAsyncTasks);
}
};// Reusable — you manage lifetime
auto* Task = new FAsyncTask<FMyComputeTask>(MoveTemp(Data));
Task->StartBackgroundTask(); // dispatches to GThreadPool
Task->EnsureCompletion(); // blocks or runs inline if not started
int32 R = Task->GetTask().Result;
delete Task;
// Fire-and-forget — auto-deletes on completion
(new FAutoDeleteAsyncTask<FMyComputeTask>(MoveTemp(Data)))->StartBackgroundTask();IsWorkDone()Cancel()StartSynchronousTask()class FMyGraphTask
{
public:
FMyGraphTask(int32 InValue) : Value(InValue) {}
static ESubsequentsMode::Type GetSubsequentsMode()
{ return ESubsequentsMode::TrackSubsequents; }
ENamedThreads::Type GetDesiredThread()
{ return ENamedThreads::AnyThread; }
TStatId GetStatId() const
{ RETURN_QUICK_DECLARE_CYCLE_STAT(FMyGraphTask, STATGROUP_TaskGraphTasks); }
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{ /* work here */ }
private:
int32 Value;
};FGraphEventArray Prerequisites; // TArray<FGraphEventRef, TInlineAllocator<4>>
Prerequisites.Add(SomePriorEvent);
FGraphEventRef TaskEvent = TGraphTask<FMyGraphTask>::CreateTask(&Prerequisites)
.ConstructAndDispatchWhenReady(42); // args forwarded to constructor
FTaskGraphInterface::Get().WaitUntilTaskCompletes(TaskEvent, ENamedThreads::GameThread);AsyncTask(ENamedThreads::GameThread, [this]()
{
MyActor->UpdateHealth(NewValue); // safe — runs on game thread
});#include "Tasks/Task.h"
UE::Tasks::TTask<int32> Task = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[]() { return ExpensiveComputation(); }
);
int32 Result = Task.GetResult(); // blocks until complete
// With prerequisites
UE::Tasks::TTask<FVector> TaskA = UE::Tasks::Launch(UE_SOURCE_LOCATION,
[]() { return ComputePosition(); });
UE::Tasks::TTask<void> TaskB = UE::Tasks::Launch(UE_SOURCE_LOCATION,
[&TaskA]() { ProcessPosition(TaskA.GetResult()); },
UE::Tasks::Prerequisites(TaskA)
);GetResult()IsCompleted()Wait()Wait(FTimespan)TryRetractAndExecute()Trigger()Async()TFuture<T>TFuture<FMyResult> Future = Async(EAsyncExecution::ThreadPool,
[]() -> FMyResult { return ComputeResult(); },
[]() { /* completion callback — runs on unspecified thread */ }
);
FMyResult R = Future.Get(); // blocks, does NOT invalidate (unlike std::future)| Mode | Thread |
|---|---|
| Worker via TaskGraph |
| Game thread via TaskGraph |
| New dedicated thread |
| |
| |
AsyncPool(GThreadPool, Lambda)AsyncThread(Lambda, StackSize, Priority)std::futureGet()Consume()std::future::get()IsReady()Wait()WaitFor(FTimespan)Then(Continuation)Next(Continuation)Share()TPromise<FMyData> Promise;
TFuture<FMyData> Future = Promise.GetFuture(); // call once
Async(EAsyncExecution::ThreadPool, [P = MoveTemp(Promise)]() mutable
{
P.SetValue(GenerateData()); // or EmplaceValue()
});
FMyData Result = Future.Get(); // blocks on game threadParallelForParallelFor(Meshes.Num(), [&Meshes](int32 Index)
{
ProcessMesh(Meshes[Index]);
});
// With MinBatchSize — prevents overhead for small workloads
ParallelFor(TEXT("ProcessMeshes"), Meshes.Num(), 64,
[&Meshes](int32 Index) { ProcessMesh(Meshes[Index]); }
);| Flag | Value | Effect |
|---|---|---|
| 0 | Default behavior |
| 1 | Debug: run sequentially |
| 2 | Iterations have variable cost |
| 4 | Pump render commands while waiting |
| 8 | Lower priority for workers |
ParallelForWithTaskContextreferences/thread-safety-guide.mdUObject*AsyncTask(ENamedThreads::GameThread, [WeakActor = TWeakObjectPtr<AActor>(MyActor)]()
{
if (AActor* Actor = WeakActor.Get()) // nullptr if GC'd
{
Actor->UpdateFromBackgroundWork(NewData);
}
});TWeakObjectPtrUObject*TSharedPtr<T, ESPMode::ThreadSafe>ESPMode::NotThreadSafeFCriticalSectionUE::FPlatformRecursiveMutexFCriticalSection DataLock;
void AddPosition(const FVector& Pos)
{
FScopeLock Lock(&DataLock); // RAII — unlocks on scope exit
SharedPositions.Add(Pos);
}FRWLockFRWLock CacheLock;
FVector Read(FName Key) { FReadScopeLock RL(CacheLock); return Cache.FindRef(Key); }
void Write(FName K, FVector V) { FWriteScopeLock WL(CacheLock); Cache.Add(K, V); }FEventRefFEvent*FEventRef WorkReady(EEventMode::AutoReset);
WorkReady->Trigger(); // producer signals
WorkReady->Wait(); // consumer blocksFThreadSafeCounterFThreadSafeBoolstd::atomic<int32>std::atomic<bool>TQueue<FMyMessage, EQueueMode::Mpsc> MessageQueue;
// Producer (any thread)
MessageQueue.Enqueue(FMyMessage{...});
// Consumer (game thread tick)
FMyMessage Msg;
while (MessageQueue.Dequeue(Msg)) { ProcessMessage(Msg); }SpscMpscPeek()// WRONG — UObject access off game thread
Async(EAsyncExecution::ThreadPool, [this]()
{ MyActor->Health = ComputeNewHealth(); });
// RIGHT — compute off-thread, apply on game thread via weak pointer
Async(EAsyncExecution::ThreadPool, [WeakActor = TWeakObjectPtr<AActor>(MyActor)]()
{
float NewHealth = ComputeNewHealth();
AsyncTask(ENamedThreads::GameThread, [WeakActor, NewHealth]()
{ if (AActor* A = WeakActor.Get()) { A->SetHealth(NewHealth); } });
});// WRONG — non-atomic refcount
auto Data = MakeShared<FMyData>();
// RIGHT
auto Data = MakeShared<FMyData, ESPMode::ThreadSafe>();// WRONG — FRWLock is NOT recursive
FReadScopeLock Outer(Lock);
FReadScopeLock Inner(Lock); // DEADLOCK on some platforms
// RIGHT — acquire once, do all reads, release// WRONG — concurrent writes
int32 Total = 0;
ParallelFor(Data.Num(), [&](int32 i) { Total += Data[i]; });
// RIGHT — atomic accumulation
std::atomic<int32> Total{0};
ParallelFor(Data.Num(), [&](int32 i)
{ Total.fetch_add(Data[i], std::memory_order_relaxed); });ue-cpp-foundationsue-gameplay-frameworkue-networking-replicationue-data-assets-tables