Header: #include "AsyncFlowThreadAwaiters.h"
All threading awaiters guarantee that the coroutine resumes on the game thread unless you explicitly migrate with
MoveToThread / MoveToTask.
Rule: UObject access, GC interaction, and world queries are forbidden inside background lambdas. Only pure computation should run off the game thread.
v2 note:
TTaskitself has zero tick dependency. Thread awaiters, sync primitives, and delegate awaiters all work withoutUAsyncFlowTickSubsystemor any other subsystem. Only timing awaiters (delays, conditions, tick counts) require the tick subsystem.
Offload a lambda to the thread pool. The coroutine suspends, the lambda runs on a worker, and the coroutine resumes on the game thread with the result.
int32 Result = co_await AsyncFlow::RunOnBackgroundThread([]()
{
return ExpensivePathfindingCalc();
});
// Back on game thread with the result
co_await AsyncFlow::RunOnBackgroundThread([]()
{
CompressData(Buffer);
});
// Back on game threadRunOnBackgroundThread is [[nodiscard]] — calling it without co_await is a compile warning.
Exceptions thrown inside the Work lambda are caught on the background thread and marshaled back to the game thread.
They are re-thrown at the co_await site in the coroutine.
AsyncFlow::TTask<void> UMyComponent::ProcessData()
{
try
{
FResult Result = co_await AsyncFlow::RunOnBackgroundThread([]()
{
if (!LoadData())
{
throw std::runtime_error("Failed to load data");
}
return ComputeResult();
});
ApplyResult(Result);
}
catch (const std::exception& Ex)
{
UE_LOG(LogTemp, Error, TEXT("Background work failed: %s"), UTF8_TO_TCHAR(Ex.what()));
}
}This guarantee applies only to RunOnBackgroundThread. AwaitFuture and the thread migration awaiters (MoveToThread,
MoveToTask, MoveToNewThread) do not catch exceptions — an unhandled exception from those contexts will terminate the
process.
Several UE engine types are directly co_await-able without explicit wrapper functions:
TFuture<FString> Future = SomeAsyncAPI();
FString Result = co_await Future; // directly awaitable — no wrapper neededVoid futures:
TFuture<void> Future = SomeAsyncVoidAPI();
co_await Future;UE::Tasks::TTask<int32> UETask = UE::Tasks::Launch(
TEXT("Compute"),
[]() { return 42; }
);
int32 Result = co_await UETask; // directly awaitableVoid tasks (UE::Tasks::FTask):
UE::Tasks::FTask VoidTask = UE::Tasks::Launch(
TEXT("Work"),
[]() { DoSomething(); }
);
co_await VoidTask;co_await OnTakeDamageDelegate; // multicast — directly awaitable
co_await MyUnicastDelegate; // unicast — directly awaitableAny DECLARE_DYNAMIC_MULTICAST_DELEGATE type can be awaited implicitly inside an AsyncFlow coroutine:
// Implicit — just co_await the delegate reference
co_await MyActor->OnSomeEvent;Or explicitly via the wrapper:
co_await AsyncFlow::WaitForDynamicDelegate(MyActor->OnSomeEvent);This works by creating a transient bridge UObject with a UFUNCTION that binds to the dynamic delegate. The binding is
automatically cleaned up after the first broadcast or on cancellation.
Note: Only the "delegate fired" event is captured — parameters are not forwarded. For typed dynamic delegates with parameters, use
AsyncFlow::Chain()with manual binding.
The explicit wrappers (
AwaitFuture,AwaitUETask,WaitForDelegate) are still available for backward compatibility or when you need to pass additional options.
Wrap an existing TFuture<T> as a co_awaitable. Blocks a thread-pool worker until the future resolves, then resumes on
the game thread.
Note:
TFuture<T>is now directlyco_await-able (see above). This explicit wrapper is still available but no longer required.
TFuture<FString> Future = SomeAsyncAPI();
FString Result = co_await AsyncFlow::AwaitFuture(MoveTemp(Future));Void futures:
TFuture<void> Future = SomeAsyncVoidAPI();
co_await AsyncFlow::AwaitFuture(MoveTemp(Future));Run a ParallelFor on a background thread. Each iteration calls Body(Index). The coroutine resumes on the game thread
when all iterations finish.
TArray<FResult> Results;
Results.SetNum(1000);
co_await AsyncFlow::ParallelForAsync(1000, [&Results](int32 Index)
{
Results[Index] = HeavyCompute(Index);
});
// All 1000 items processed, back on game threadWrap a UE::Tasks::TTask<T> as a co_awaitable. Available when Tasks/Task.h is present.
Note:
UE::Tasks::TTask<T>is now directlyco_await-able (see above). This explicit wrapper is still available but no longer required.
UE::Tasks::TTask<int32> UETask = UE::Tasks::Launch(
TEXT("Compute"),
[]() { return 42; }
);
int32 Result = co_await AsyncFlow::AwaitUETask(MoveTemp(UETask));Void tasks (UE::Tasks::FTask):
UE::Tasks::FTask VoidTask = UE::Tasks::Launch(
TEXT("Work"),
[]() { DoSomething(); }
);
co_await AsyncFlow::AwaitUETask(MoveTemp(VoidTask));Move the coroutine body between threads. Use these for sustained background computation where multiple sequential operations need to run off the game thread.
Resume the coroutine on the game thread. Use after any off-thread section to safely access UObjects again.
co_await AsyncFlow::MoveToGameThread();No-op if already on the game thread.
Resume on a specific named UE thread.
co_await AsyncFlow::MoveToThread(ENamedThreads::AnyBackgroundThreadNormalTask);
// Now running on a background thread — no UObject access
DoHeavyWork();
co_await AsyncFlow::MoveToGameThread();
// Safe againResume on a UE::Tasks worker thread.
co_await AsyncFlow::MoveToTask();
// Running on task thread pool
ComputePathGrid();
co_await AsyncFlow::MoveToGameThread();Resume on a brand-new dedicated thread. Good for blocking I/O.
co_await AsyncFlow::MoveToNewThread();
FString Data = FFileHelper::LoadFileToString(...);
co_await AsyncFlow::MoveToGameThread();
ProcessData(Data);Yield the coroutine to the game thread scheduler. No world context required. Schedules resumption via
AsyncTask(GameThread).
co_await AsyncFlow::Yield();Free-threaded delay using FPlatformProcess::Sleep on a worker thread. Resumes on the game thread. No world context or
tick subsystem required.
co_await AsyncFlow::PlatformSeconds(0.5);Tip: If you don't need to return to the game thread, use
PlatformSecondsAnyThreadinstead for lower overhead.
Records the current named-thread kind at construction and, when later co_await-ed, dispatches back to a thread of that
kind. If execution is already on the recorded kind, the awaiter is a no-op.
auto GoBack = AsyncFlow::MoveToSimilarThread(); // records "game thread"
co_await AsyncFlow::MoveToTask(); // now on a worker
// do background work ...
co_await GoBack; // back to game threadMoves execution into a specific FQueuedThreadPool.
co_await AsyncFlow::MoveToThreadPool(*GThreadPool);await_ready() always returns false — the coroutine is always dispatched to the pool.
Like PlatformSeconds but resumes on the worker thread that performed the sleep instead of dispatching back to the
game thread. More efficient for background pipelines.
co_await AsyncFlow::PlatformSecondsAnyThread(2.0);
// still on worker — call MoveToGameThread() if you need GT accessWait until FPlatformTime::Seconds() reaches an absolute target.
double Deadline = FPlatformTime::Seconds() + 5.0;
co_await AsyncFlow::UntilPlatformTime(Deadline); // resumes on game thread
co_await AsyncFlow::UntilPlatformTimeAnyThread(Deadline); // resumes on workerIf the target has already passed, await_ready() returns true and the coroutine continues without suspending.
A typical pattern for background work with intermediate game-thread access:
AsyncFlow::TTask<void> UMyComponent::ProcessLargeDataSet()
{
// Fetch data on game thread
TArray<FRawData> Data = GatherData();
// Move to background for processing
co_await AsyncFlow::MoveToTask();
TArray<FProcessedData> Processed = CrunchNumbers(Data);
// Back to game thread to apply results
co_await AsyncFlow::MoveToGameThread();
ApplyResults(Processed);
// Another background pass
co_await AsyncFlow::MoveToTask();
TArray<FCompressedData> Compressed = CompressResults(Processed);
// Final game-thread write
co_await AsyncFlow::MoveToGameThread();
SaveToSlot(Compressed);
}- All
RunOnBackgroundThread,MoveToThread,MoveToTask,MoveToNewThreadlambdas/sections run off the game thread. - UObject pointers,
GetWorld(), GC-managed memory, and any engine API that requires the game thread are forbidden in off-thread contexts. - The alive-flag pattern (
FAwaiterAliveFlag) prevents stale resumes if the coroutine frame is destroyed while the background work is in flight. ParallelFordistributes work across cores. Wrapping it inRunOnBackgroundThreadprevents it from blocking the game thread.