关于 ue-async-threading
面向 UE 5.8 开发者的异步与线程技能,帮助选择后台任务、并行计算和线程同步方案。汇总 UE::Tasks、Async、线程池、专用线程以及计时器等 API,并说明 UObject 的线程规则。
主要功能
- 01UE::Tasks 与任务依赖
- 02Async、TFuture 与游戏线程调度
- 03线程池任务和 ParallelFor
- 04FRunnable 专用线程
- 05同步原语、Ticker 与 Timer
适用场景
- 01将计算任务移出游戏线程
- 02并行处理网格或批量数据
- 03按顺序执行共享资源任务
- 04安排逐帧或延迟回调
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ quodsoler/ unreal-engine-skills --skill ue-async-threading安装后在 Claude Code 中输入/ue-async-threading确认已加载。
附带文件references/thread-safety-guide.mdreferences/threading-patterns.md
详细介绍
UE 异步与线程
目标引擎:UE 5.8。以下 API 已根据 5.8 头文件验证;旧形式列在“已弃用 — 请勿使用”下。
本技能介绍如何将工作移出游戏线程并将结果带回:UE::Tasks 系统(Tasks/Task.h、Tasks/Pipe.h)、Async/TFuture(Async/Async.h、Async/Future.h)、线程池 FAsyncTask(Async/AsyncWork.h)、ParallelFor、专用 FRunnable 线程、锁和无锁队列、FTSTicker 和 FTimerManager。除计时器外,所有内容都位于 Core 模块;FTimerManager 位于 Engine;ENQUEUE_RENDER_COMMAND 位于 RenderCore。Build.cs:PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" });,仅在入队渲染命令时添加 "RenderCore"。
上下文
如果 .agents/ue-project-context.md 存在,请阅读它(模块名称、约定、已启用的插件、GAS/网络设置)。如果文件不存在,也不要停止。
根据请求和代码库确定相关领域。只有当两种合理的理解会导致不同代码时才提问。
| 请求涉及…… | 请参阅 |
|---|---|
| 适用哪种 API | 模式选择 |
| 一次性后台工作、链式调用、前置任务、事件 | UE::Tasks |
| 无需专用线程即可串行访问资源 | FPipe 和 FTaskConcurrencyLimiter |
| Future、Promise、派发到游戏线程 | Async、TFuture 和 AsyncTask |
| 可复用的池化工作单元 | FAsyncTask 和 FAutoDeleteAsyncTask |
| 数据并行循环 | ParallelFor |
| 长期运行的专用线程 | FRunnable 和 FRunnableThread |
| 锁、事件、原子操作、队列、共享指针 | 同步 |
| 每帧回调和延迟调用 | Ticker 和 Timer |
| UObject、GC 和渲染线程规则 | 线程安全规则 |
| 旧形式 | 已弃用 — 请勿使用 |
线程模型
| 线程 | 检查方式 | 所有权 |
|---|---|---|
| 游戏线程 | IsInGameThread()(CoreGlobals.h) | 所有 UObject 访问、Blueprint、游戏逻辑、计时器、Ticker |
| 渲染线程 | IsInRenderingThread() | 场景代理、渲染命令(ENQUEUE_RENDER_COMMAND) |
| RHI 线程 | IsInRHIThread() | GPU 命令提交 |
| 工作线程 | 无 | UE::Tasks 调度器、TaskGraph AnyThread、ParallelFor |
| 线程池 | 无 | GThreadPool、GBackgroundPriorityThreadPool、GIOThreadPool;GLargeThreadPool 仅限 WITH_EDITOR(Misc/QueuedThreadPool.h) |
黄金法则:UObject 只能在游戏线程上操作。在线程外使用普通数据进行计算,然后通过 TWeakObjectPtr 在游戏线程上应用结果(参见线程安全规则)。
模式选择
| 需求 | 使用 | 结果 |
|---|---|---|
| 一次性后台工作、依赖、链式调用 | UE::Tasks::Launch | TTask<T> |
| 从任意位置在游戏线程上运行 lambda | AsyncTask(ENamedThreads::GameThread, ...) | 无 |
| 可选择执行上下文的 Future 风格结果 | Async(EAsyncExecution, ...) | TFuture<T> |
| 串行执行访问同一资源的任务(FIFO) | UE::Tasks::FPipe | TTask<T> |
| 限制同时运行的任务数量 | UE::Tasks::FTaskConcurrencyLimiter | 无 |
| 由所有者管理生命周期的可复用池化工作单元 | FAsyncTask<T> / FAutoDeleteAsyncTask<T> | 通过 GetTask() |
| 数据并行循环,调用方阻塞等待 | ParallelFor | 无 |
| 长期运行的线程(套接字、文件监视器、模拟循环) | FRunnable + FRunnableThread::Create | 手动管理 |
| Actor Tick 之外的每帧回调 | FTSTicker::GetCoreTicker().AddTicker | 句柄 |
| 在游戏线程上延迟调用或重复调用 | FTimerManager::SetTimer | FTimerHandle |
UE::Tasks
#include "Tasks/Task.h"
// Tasks/Task.h:299
template<typename TaskBodyType>
UE::Tasks::TTask<TInvokeResult_T<TaskBodyType>> Launch(const TCHAR* DebugName, TaskBodyType&& TaskBody,
ETaskPriority Priority = ETaskPriority::Normal,
EExtendedTaskPriority ExtendedPriority = EExtendedTaskPriority::None,
ETaskFlags Flags = ETaskFlags::None);
// Tasks/Task.h:324 — same, with a prerequisites collection as the third parameter
template<typename TaskBodyType, typename PrerequisitesCollectionType>
UE::Tasks::TTask<TInvokeResult_T<TaskBodyType>> Launch(const TCHAR* DebugName, TaskBodyType&& TaskBody,
PrerequisitesCollectionType&& Prerequisites,
ETaskPriority Priority = ETaskPriority::Normal,
EExtendedTaskPriority ExtendedPriority = EExtendedTaskPriority::None,
ETaskFlags Flags = ETaskFlags::None);
using namespace UE::Tasks;
TArray<int32> Data;
TTask<int32> Sum = Launch(UE_SOURCE_LOCATION, [Data]() { return ComputeSum(Data); });
// Prerequisites: TaskB runs after TaskA completes. Handles are copyable; GetResult() is non-const.
TTask<FVector> TaskA = Launch(UE_SOURCE_LOCATION, []() { return FVector(1.f, 2.f, 3.f); });
TTask<void> TaskB = Launch(UE_SOURCE_LOCATION,
[TaskA]() mutable { const FVector Pos = TaskA.GetResult(); ConsumePosition(Pos); },
Prerequisites(TaskA), ETaskPriority::BackgroundNormal);
// Manual gate: nothing after the event starts until Trigger()
FTaskEvent Gate{ UE_SOURCE_LOCATION };
TTask<void> Gated = Launch(UE_SOURCE_LOCATION, []() { DoWork(); }, Prerequisites(Gate));
Gate.Trigger();
// Wait for a group, with timeout (Tasks/Task.h:393)
TArray<FTask> Group{ TaskB, Gated };
const bool bAllDone = Wait(Group, FTimespan::FromMilliseconds(5.0));
// Cooperative cancellation (Tasks/Task.h: FCancellationToken)
FCancellationToken Token;
Launch(UE_SOURCE_LOCATION, [&Token]() { for (int32 i = 0; i < 1000; ++i) { if (Token.IsCanceled()) { return; } Step(i); } });
Token.Cancel();
// Already-completed task holding a value (useful for uniform interfaces)
TTask<int32> Ready = MakeCompletedTask<int32>(42);
句柄 API(Tasks/Task.h:36-95):IsValid()、IsCompleted()、Wait()、Wait(FTimespan Timeout) 返回 bool;如果任务尚未开始,TryRetractAndExecute() 会内联运行该任务;GetResult() 会等待,然后返回 ResultType&(check(IsValid()))。自由函数:Wait(FTask&)、Wait(Collection, FTimespan)、WaitAny(Collection, Timeout) 返回索引;Any(Collection) 返回一个在任一输入任务完成时完成的 FTask;在正在运行的任务内部调用 AddNested(Task),使父任务在嵌套任务完成前保持未完成状态。
优先级(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(以及 LocalQueue 变体)。ETaskFlags::None | DoNotRunInsideBusyWait。传入 EExtendedTaskPriority::GameThreadNormalPri 会使任务体在游戏线程上运行。
旧版 TaskGraph 的 FGraphEventRef 可直接作为 Launch 的先决条件参数(Tasks/TaskPrivate.h:266),而由它们组成的集合也可用于 WaitAny/Any(Tasks/Task.h:417, 468),因此 UE::Tasks 可以等待 TaskGraph 工作。完整的链式示例:threading-patterns.md。
FPipe 和 FTaskConcurrencyLimiter
FPipe 会依次执行其任务(没有额外先决条件时按 FIFO 顺序执行),因此可用于替代负责保护资源的专用线程。管道必须比它的最后一个任务存活得更久;~FPipe() 会断言 !HasWork()。
#include "Tasks/Pipe.h"
#include "Tasks/TaskConcurrencyLimiter.h"
UE::Tasks::FPipe SavePipe{ TEXT("SavePipe") }; // explicit FPipe(const TCHAR* InDebugName)
UE::Tasks::TTask<void> First = SavePipe.Launch(UE_SOURCE_LOCATION, []() { WriteChunk(0); });
UE::Tasks::TTask<bool> Second = SavePipe.Launch(UE_SOURCE_LOCATION, []() { return WriteChunk(1); },
UE::Tasks::ETaskPriority::BackgroundNormal);
const bool bInsidePipe = SavePipe.IsInContext(); // true only while a pipe task runs on this thread
SavePipe.WaitUntilEmpty(); // before destroying the pipe
UE::Tasks::FTaskConcurrencyLimiter Limiter(4 /*MaxConcurrency*/, UE::Tasks::ETaskPriority::BackgroundHigh);
for (int32 Index = 0; Index < 64; ++Index)
{
Limiter.Push(UE_SOURCE_LOCATION, [Index](uint32 Slot) { ProcessWithScratch(Index, Slot); }); // Slot in [0, MaxConcurrency)
}
Limiter.Wait(); // Wait(FTimespan Timeout = FTimespan::MaxValue())
FPipe::Launch(const TCHAR*, TaskBody, [Prerequisites,] ETaskPriority = Default, EExtendedTaskPriority = None, ETaskFlags = None)(Tasks/Pipe.h:63,90)。FTaskConcurrencyLimiter 可以在其任务完成之前销毁;它的 Wait 条件满足后只会触发一次,不会重新启用(Tasks/TaskConcurrencyLimiter.h:171-212)。
Async、TFuture 和 AsyncTask
#include "Async/Async.h"
// Async/Async.h:299
template<typename CallableType>
auto Async(EAsyncExecution Execution, CallableType&& Callable, TUniqueFunction<void()> CompletionCallback = nullptr) -> TFuture<decltype(Forward<CallableType>(Callable)())>;
// Async/Async.h:407 — takes a reference, so pass *GThreadPool
template<typename CallableType>
auto AsyncPool(FQueuedThreadPool& ThreadPool, CallableType&& Callable, TUniqueFunction<void()> CompletionCallback = nullptr, EQueuedWorkPriority InQueuedWorkPriority = EQueuedWorkPriority::Normal);
// Async/Async.h:430
template<typename CallableType>
auto AsyncThread(CallableType&& Callable, uint32 StackSize = 0, EThreadPriority ThreadPri = TPri_Normal, TUniqueFunction<void()> CompletionCallback = nullptr);
// Async/Async.h:463
CORE_API void AsyncTask(ENamedThreads::Type Thread, TUniqueFunction<void()> Function);
EAsyncExecution(Async/Async.h:27) | 运行位置 |
|---|---|
TaskGraph | 工作线程,适用于短任务 |
TaskGraphMainThread | 游戏线程,可能在 GC 或 PostLoad 等待期间运行——仅用于可在任何位置安全运行的代码 |
TaskGraphMainTick | 游戏线程的 Tick 期间——委托和 UObject 代码的安全选择 |
Thread | 新建的专用线程,适用于长时间运行或阻塞式 I/O |
ThreadIfForkSafe | 与 Thread 相同,并支持 fork |
ThreadPool | GThreadPool |
LargeThreadPool | GLargeThreadPool,仅限 WITH_EDITOR |
TFuture<FMyResult> Future = Async(EAsyncExecution::ThreadPool, []() { return ComputeResult(); });
if (Future.IsReady()) { UseResult(Future.Get()); } // non-blocking check
FMyResult Copy = Future.Get(); // blocks; does NOT invalidate (Async/Future.h:226)
Future.Next([](FMyResult Value) { UseResult(Value); }); // continuation receives the value; runs on the completing thread
TPromise<FMyResult> Promise;
TFuture<FMyResult> FromPromise = Promise.GetFuture(); // call once
Async(EAsyncExecution::Thread, [P = MoveTemp(Promise)]() mutable { P.SetValue(ComputeResult()); });
AMyActor* MyActor = FindMyActor();
AsyncTask(ENamedThreads::GameThread, [WeakActor = TWeakObjectPtr<AMyActor>(MyActor), Copy]()
{
if (AMyActor* Actor = WeakActor.Get()) { Actor->ApplyResult(Copy); }
});
TFuture<T>(Async/Future.h:210-440):Get()、IsReady()、IsValid()、Wait()、WaitFor(const FTimespan&)、WaitUntil(const FDateTime&)、Then(Func) 接收 TFuture<T>、Next(Func) 接收 T(两者都会移出状态并使此 future 失效,Async/Future.h:669)、Consume() 会移出值并使 future 失效、Share() 会返回 TSharedFuture<T>、Reset()。TPromise<T>(:527):GetFuture()、SetValue(const T&)、SetValue(T&&)、EmplaceValue(Args&&...)。延续会在完成 promise 的线程上运行——需要时请显式切换到游戏线程。
FAsyncTask 和 FAutoDeleteAsyncTask
在 FQueuedThreadPool 上运行的可复用工作单元。派生自 FNonAbandonableTask(Async/AsyncWork.h:666),实现 DoWork() 和 GetStatId()。FAsyncTask<T> 会在自己的构造函数中构造 T,因此使用 friend 声明后,T 的构造函数可以是私有的(引擎在 AsyncWork.h:26-42 中的示例就是这样做的)。代码通过 Task->GetTask() 读取的成员必须是 public,因为访问发生在友元之外。
#include "Async/AsyncWork.h"
class FMyChunkTask : public FNonAbandonableTask
{
public:
friend class FAsyncTask<FMyChunkTask>;
friend class FAutoDeleteAsyncTask<FMyChunkTask>;
explicit FMyChunkTask(TArray<int32> InInput) : Input(MoveTemp(InInput)) {}
int32 Result = 0;
void DoWork()
{
for (int32 Value : Input) { Result += Value; }
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FMyChunkTask, STATGROUP_ThreadPoolAsyncTasks);
}
private:
TArray<int32> Input;
};
// Owner-managed lifetime
TArray<int32> Numbers;
FAsyncTask<FMyChunkTask>* Task = new FAsyncTask<FMyChunkTask>(MoveTemp(Numbers));
Task->StartBackgroundTask(); // (FQueuedThreadPool* = GThreadPool, EQueuedWorkPriority = Normal, EQueuedWorkFlags = None, int64 RequiredMemory = -1, const TCHAR* DebugName = nullptr)
const bool bReady = Task->IsDone(); // poll once per frame, never spin
Task->EnsureCompletion(); // (bool bDoWorkOnThisThreadIfNotStarted = true, bool bIsLatencySensitive = false)
const int32 Sum = Task->GetTask().Result;
delete Task;
// Fire-and-forget: deletes itself after DoWork
(new FAutoDeleteAsyncTask<FMyChunkTask>(MoveTemp(Numbers)))->StartBackgroundTask();
其他成员(Async/AsyncWork.h:415-558):StartSynchronousTask(...) 会内联运行;如果任务仍在队列中,Cancel() 返回 true;WaitCompletionWithTimeout(float TimeLimitSeconds);IsWorkDone() 是开销较低的非阻塞检查。低优先级工作请将 GBackgroundPriorityThreadPool 作为线程池传入。完整模板:threading-patterns.md。
ParallelFor
#include "Async/ParallelFor.h"
// Async/ParallelFor.h:526, :543
inline void ParallelFor(int32 Num, TFunctionRef<void(int32)> Body, EParallelForFlags Flags = EParallelForFlags::None);
inline void ParallelFor(const TCHAR* DebugName, int32 Num, int32 MinBatchSize, TFunctionRef<void(int32)> Body, EParallelForFlags Flags = EParallelForFlags::None);
// Async/ParallelFor.h:792 — Body is called as Body(ContextType&, int32 Index)
template <typename ContextType, typename ContextAllocatorType, typename FunctionType>
inline void ParallelForWithTaskContext(const TCHAR* DebugName, TArray<ContextType, ContextAllocatorType>& OutContexts, int32 Num, int32 MinBatchSize, const FunctionType& Body, EParallelForFlags Flags = EParallelForFlags::None);
TArray<UStaticMesh*> Meshes;
ParallelFor(Meshes.Num(), [&Meshes](int32 Index) { ProcessMesh(Meshes[Index]); });
ParallelFor(TEXT("ProcessMeshes"), Meshes.Num(), 64, [&Meshes](int32 Index) { ProcessMesh(Meshes[Index]); },
EParallelForFlags::Unbalanced | EParallelForFlags::BackgroundPriority);
struct FMyScratch { TArray<FVector> Buffer; };
TArray<FMyScratch> Contexts; // one per worker task, reused across iterations
ParallelForWithTaskContext(TEXT("Normals"), Contexts, Meshes.Num(), 32,
[&Meshes](FMyScratch& Scratch, int32 Index) { Scratch.Buffer.Reset(); ComputeNormals(Meshes[Index], Scratch.Buffer); });
EParallelForFlags(Async/ParallelFor.h:46) | 效果 |
|---|---|
None | 默认 |
ForceSingleThread | 在调用线程上顺序运行(用于调试) |
Unbalanced | 各次迭代的开销差异很大;使用更小的批次 |
PumpRenderingThread | 调用线程在等待期间处理渲染命令 |
BackgroundPriority | 工作线程以后台优先级运行 |
另外还有:ParallelForTemplate(...)(没有 TFunctionRef 间接调用,:496)、ParallelForWithPreWork(...)(:571,在调用线程上先执行工作,然后再参与处理)、ParallelForWithTaskContext(OutContexts, Num, ContextConstructor, Body, Flags)(:722)、ParallelForWithExistingTaskContext(TArrayView<ContextType> Contexts, Num, MinBatchSize, Body, Flags)(:815)。调用线程也会参与执行,并一直阻塞到所有迭代完成。CVar Async.ParallelFor.DisableOversubscription(GParallelForDisableOversubscription,Async/ParallelFor.h:43)会阻止 ParallelFor 唤醒额外的工作线程。
FRunnable 和 FRunnableThread
仅用于专用的长生命周期线程。新线程上的生命周期为:Init() → Run() → Exit()。外部通过 Kill() 调用 Stop();它只能发出信号。
// HAL/Runnable.h:32-69 — override verbatim
virtual bool Init();
virtual uint32 Run() = 0;
virtual void Stop();
virtual void Exit();
virtual class FSingleThreadRunnable* GetSingleThreadInterface(); // return a fallback for -nothreading platforms, or nullptr
// HAL/RunnableThread.h:44
static CORE_API FRunnableThread* Create(
class FRunnable* InRunnable,
const TCHAR* ThreadName,
uint32 InStackSize = 0,
EThreadPriority InThreadPri = TPri_Normal,
uint64 InThreadAffinityMask = FPlatformAffinity::GetNoAffinityMask(),
EThreadCreateFlags InCreateFlags = EThreadCreateFlags::None);
virtual bool Kill(bool bShouldWait = true) = 0; // calls Stop(); with bShouldWait blocks until Run() returns
virtual void WaitForCompletion() = 0;
virtual void Suspend(bool bShouldPause = true) = 0;
virtual void SetThreadPriority(EThreadPriority NewPriority) = 0;
EThreadPriority(GenericPlatform/GenericPlatformAffinity.h:25):TPri_Normal, TPri_AboveNormal, TPri_BelowNormal, TPri_Highest, TPri_Lowest, TPri_SlightlyBelowNormal, TPri_TimeCritical。始终先调用 Kill(true),再 delete FRunnableThread*;不等待就终止会导致泄漏,并可能造成死锁(头文件注释 HAL/RunnableThread.h:77-79)。使用 FPlatformProcess::Sleep(float Seconds) 休眠,或通过 FEventRef 阻塞,而不是忙等。包含关闭逻辑的完整模板:threading-patterns.md。
同步
| 需求 | 类型 | RAII 守卫 | 头文件 |
|---|---|---|---|
| 通用互斥锁,可递归 | FCriticalSection(= UE::FPlatformRecursiveMutex) | FScopeLock Lock(&Mutex);在作用域内释放时使用 FScopeUnlock | HAL/CriticalSection.h:53、Misc/ScopeLock.h:140 |
| 多读者、单写者,不可递归 | FRWLock(= UE::FPlatformRWLock) | FReadScopeLock(FRWLock&)、FWriteScopeLock(FRWLock&)、FRWScopeLock(Lock, SLT_ReadOnly / SLT_Write) | HAL/CriticalSection.h:56、Misc/ScopeRWLock.h:92-198 |
| 单字节、不可递归、不公平,速度最快 | UE::FMutex | UE::TUniqueLock<UE::FMutex>;使用 UE::DeferLock 的 UE::TDynamicUniqueLock | Async/Mutex.h:18、Async/UniqueLock.h:19,48、Async/LockTags.h:12 |
| 小型递归互斥锁 | UE::FRecursiveMutex | UE::TUniqueLock | Async/RecursiveMutex.h:19 |
| 小型读写锁 | UE::FSharedMutex(LockShared/UnlockShared) | UE::TSharedLock、UE::TUniqueLock | Async/SharedMutex.h:22、Async/SharedLock.h:21 |
为任何具有 Lock()/Unlock() 的类型添加守卫 | UE::TScopeLock<MutexType> | — | Misc/ScopeLock.h:25 |
TArray<FVector> Points;
mutable FCriticalSection Mutex; // mutable so const getters can lock
void Add(const FVector& P) { FScopeLock Lock(&Mutex); Points.Add(P); }
TMap<FName, FVector> Cache;
mutable FRWLock CacheLock;
FVector Read(FName Key) const { FReadScopeLock Lock(CacheLock); return Cache.FindRef(Key); }
void Write(FName Key, FVector V) { FWriteScopeLock Lock(CacheLock); Cache.Add(Key, V); }
int32 Counter = 0;
UE::FMutex SmallMutex;
void Bump() { UE::TUniqueLock Lock(SmallMutex); ++Counter; }
事件: FEventRef Event(EEventMode::AutoReset)(HAL/Event.h:129-139)是 RAII 管理的池化 FEvent:Event->Trigger()、Event->Wait()、Event->Wait(uint32 WaitTimeMs)、Event->Reset()。直接使用池:FEvent* E = FPlatformProcess::GetSynchEventFromPool(bool bIsManualReset = false) / FPlatformProcess::ReturnSynchEventToPool(E)(GenericPlatformProcess.h:786,799)。UE::FManualResetEvent(Async/ManualResetEvent.h):Notify()、Wait()、WaitFor(FMonotonicTimeSpan)、Reset()。任务之间优先使用 UE::Tasks::FTaskEvent——等待它不会阻塞工作线程。
原子变量: std::atomic<T>(<atomic>,已由 Templates/Atomic.h 包含)。FThreadSafeCounter、FThreadSafeBool 和 TAtomic 在各自的头文件中已标记为弃用(参见已弃用——请勿使用)。仅用于标志和计数器时使用 std::memory_order_relaxed;原子变量用于发布其他数据时使用 acquire/release。
队列: TMpscQueue<T>(Containers/MpscQueue.h)和 TSpscQueue<T>(Containers/SpscQueue.h):Enqueue(Args&&...)、bool Dequeue(T& Out)、TOptional<T> Dequeue()、T* Peek()、IsEmpty()。仅支持单个消费者。TQueue<T, EQueueMode> 仍可编译,但已标记为“计划弃用”(Containers/Queue.h:11)。
共享指针: TSharedPtr、TSharedRef、TWeakPtr 和 MakeShared 默认使用 ESPMode::ThreadSafe(Templates/SharedPointerFwd.h:24-27、SharedPointer.h:2110);引用计数是原子的,但指向对象不受保护。仅在单线程高频路径中选择 ESPMode::NotThreadSafe。
Ticker 和定时器
两者都在游戏线程上运行。FTSTicker 是引擎级的,会在关卡切换后继续运行;FTimerManager 属于单个 UWorld,并会随其暂停。
#include "Containers/Ticker.h"
// Inside AMyActor, which declares: void Poll(float DeltaTime); void OnFire();
// Containers/Ticker.h:45,56,66 — delegate returns true to keep ticking, false to remove itself
FTSTicker::FDelegateHandle TickHandle = FTSTicker::GetCoreTicker().AddTicker(
FTickerDelegate::CreateWeakLambda(this, [this](float DeltaTime) { Poll(DeltaTime); return true; }), 0.0f /*InDelay*/);
FTSTicker::FDelegateHandle Named = FTSTicker::GetCoreTicker().AddTicker(TEXT("MyPoll"), 0.5f, [](float DeltaTime) { return true; });
FTSTicker::RemoveTicker(TickHandle); // static; safe with an expired handle
FTickerDelegate 是 DECLARE_DELEGATE_RetVal_OneParam(bool, FTickerDelegate, float)(Containers/Ticker.h:21)。子类化 FTSTickerObjectBase 并重写 virtual bool Tick(float DeltaTime) = 0,即可创建会自行注册的对象(:136-158)。
#include "TimerManager.h"
// Engine/Public/TimerManager.h:167-237, 247-268, 281-291
FTimerHandle FireHandle, OnceHandle, DelegateHandle; // normally UPROPERTY-free members of AMyActor
FTimerManager& Timers = GetWorldTimerManager(); // AActor; elsewhere GetWorld()->GetTimerManager()
Timers.SetTimer(FireHandle, this, &AMyActor::OnFire, 1.0f, /*InbLoop*/ true, /*InFirstDelay*/ -1.f);
Timers.SetTimer(OnceHandle, FTimerDelegate::CreateWeakLambda(this, [this]() { OnFire(); }), 2.0f, false);
Timers.SetTimer(DelegateHandle, FTimerDelegate::CreateUObject(this, &AMyActor::OnFire), 1.0f, false, 0.25f);
FTimerManagerTimerParameters Params; // TimerManager.h:124
Params.bLoop = true; Params.bMaxOncePerFrame = true; Params.FirstDelay = 0.5f;
Timers.SetTimer(FireHandle, this, &AMyActor::OnFire, 0.1f, Params);
FTimerHandle NextTick = Timers.SetTimerForNextTick(this, &AMyActor::OnFire);
Timers.PauseTimer(FireHandle); Timers.UnPauseTimer(FireHandle);
const float Remaining = Timers.GetTimerRemaining(FireHandle); // -1 if not found
Timers.ClearTimer(FireHandle); // invalidates the handle
Timers.ClearAllTimersForObject(this); // clears timers bound to this object; CreateLambda/TFunction timers need ClearTimer(Handle)
FTimerDelegate 是 TDelegate<void(), FNotThreadSafeNotCheckedDelegateUserPolicy>(TimerManager.h:23);使用 CreateUObject、CreateWeakLambda、CreateLambda、CreateSP、CreateStatic 创建(Delegates/DelegateSignatureImpl.inl)。SetTimer 的重载接受方法指针、FTimerDelegate、FTimerDynamicDelegate、TFunction<void(void)>&&,或不传回调(仅倒计时句柄)。面向 Blueprint:UKismetSystemLibrary::K2_SetTimer(UObject* Object, FString FunctionName, float Time, bool bLooping, bool bMaxOncePerFrame = false, float InitialStartDelay = 0.f, float InitialStartDelayVariance = 0.f) 和 K2_ClearAndInvalidateTimerHandle(const UObject* WorldContextObject, UPARAM(ref) FTimerHandle& Handle)(Kismet/KismetSystemLibrary.h:902,832)。定时器示例:threading-patterns.md。
线程安全规则
- **仅限游戏线程:**任何
UPROPERTY读写、UFUNCTION调用、GetWorld()、生成、销毁、组件变更、UObject 上的委托、计时器、ticker。使用check(IsInGameThread())为入口添加保护。 - **绝不要将原始
UObject*或this捕获到延迟工作中。**捕获TWeakObjectPtr<T>(UObject/WeakObjectPtrTemplates.h:25),并在游戏线程上通过Get()解析,或使用CreateWeakLambda构建委托。 - GC 可能在启动任务和回调之间运行。
FGCScopeGuard(UObject/GarbageCollection.h:117)会在作用域内阻止 GC;仅将其用于工作线程上的短时只读访问,绝不要在阻塞等待期间使用。 - 渲染线程:
ENQUEUE_RENDER_COMMAND(MyCommand)([Data](FRHICommandListImmediate& RHICmdList) { UploadOnRenderThread(RHICmdList, Data); });(RenderCore/Public/RenderingThread.h:1087,模块RenderCore);从游戏线程调用FlushRenderingCommands()可排空渲染命令。使用IsInRenderingThread()检查。 - 共享数据需要自己的锁,即使它位于线程安全的
TSharedPtr中也不例外;引用计数是原子的,但负载不是。 - 优先使用无锁交接:
TMpscQueue、std::atomic、双缓冲,或为每个资源使用一个FPipe。
完整模式、锁顺序、双缓冲、FScopedSlowTask 和消毒器说明:thread-safety-guide.md。异步资产加载(FStreamableManager::RequestAsyncLoad)应归入 ue-data-assets-tables。
已弃用 — 请勿使用
| 不要输出 | 在 5.8 中使用 | 来源 |
|---|---|---|
FTicker::GetCoreTicker() | FTSTicker::GetCoreTicker() | 5.8 头文件中没有 FTicker;只有 FTSTicker(Containers/Ticker.h:26) |
FThreadSafeCounter | std::atomic<int32> | 头文件注释为“已弃用。请使用 std::atomic<int32>”(HAL/ThreadSafeCounter.h:9) |
FThreadSafeBool | std::atomic<bool> | 头文件注释为“已弃用”(HAL/ThreadSafeBool.h:9) |
TAtomic<T> | std::atomic<T> | “计划弃用”(Templates/Atomic.h:13、:528);尚无 UE_DEPRECATED 宏 |
FExternalMutex | TIntrusiveMutex<Params>(Async/IntrusiveMutex.h:60) | 在 Async/ExternalMutex.h:73 中标记为 UE_DEPRECATED(5.7) |
TExternalMutex<Params> | TIntrusiveMutex<Params> | 在 Async/ExternalMutex.h:23 中标记为 UE_DEPRECATED(5.8) |
FPlatformProcess::CreateSynchEvent(...) | GetSynchEventFromPool / ReturnSynchEventToPool,或 FEventRef | 在 GenericPlatform/GenericPlatformProcess.h:776 中标记为 UE_DEPRECATED(5.0) |
TQueue<T, EQueueMode::Mpsc> / EQueueMode::Spsc | TMpscQueue<T> / TSpscQueue<T> | “计划弃用”(Containers/Queue.h:11) |
ParallelFor(Num, Body, bool bForceSingleThread, bool bPumpRenderingThread) | ParallelFor(Num, Body, EParallelForFlags) | bool 重载保留在 Async/ParallelFor.h:481;文档推荐使用 flags 形式 :526 |
AsyncPool(GThreadPool, ...) | AsyncPool(*GThreadPool, ...) | 参数类型为 FQueuedThreadPool&(Async/Async.h:407);指针形式无法编译 |
TSharedPtr<T, ESPMode::NotThreadSafe>,“因为默认值不是线程安全的” | TSharedPtr<T> — 默认值是 ESPMode::ThreadSafe | Templates/SharedPointerFwd.h:25 |
新任务使用 TGraphTask<T> / FGraphEventRef | UE::Tasks::Launch / FTask(建议,并非弃用) | Async/TaskGraphInterfaces.h 中没有 UE_DEPRECATED;FGraphEventRef 仍可作为 UE::Tasks 前置条件使用(Tasks/Task.h:360) |
常见错误
**在工作线程中访问 UObject:**垃圾回收和其他游戏线程写入会与你发生竞争。
// WRONG — Health is a UPROPERTY on this AMyActor
Async(EAsyncExecution::ThreadPool, [this]() { Health = ComputeHealth(); });
// RIGHT
Async(EAsyncExecution::ThreadPool, [Weak = TWeakObjectPtr<AMyActor>(this)]()
{
const float NewHealth = ComputeHealth();
AsyncTask(ENamedThreads::GameThread, [Weak, NewHealth]() { if (AMyActor* A = Weak.Get()) { A->ApplyResult(NewHealth); } });
});
**启动后立即阻塞游戏线程:**在下一行调用 Task.GetResult() 或 Future.Get() 会把异步操作变成同步操作。在 Tick 中轮询 IsCompleted()/IsReady(),使用 Prerequisites 链接任务,或通过 AsyncTask(ENamedThreads::GameThread, Lambda) 切回游戏线程。
在 FNonAbandonableTask 中使用私有成员:friend class FAsyncTask<T> 覆盖构造函数(它会在 FAsyncTask 内部运行),但不包括从你的代码中读取 GetTask().Result。请将结果成员设为 public。
嵌套获取 FRWLock:FRWLock 不可递归;在读锁内再次获取读锁(或在读锁内获取写锁)会导致死锁。每条调用路径只获取一次,或改用 FCriticalSection。
在 ParallelFor 中使用共享可变状态:
TArray<int32> Data;
// WRONG
int32 Total = 0; ParallelFor(Data.Num(), [&](int32 i) { Total += Data[i]; });
// RIGHT
std::atomic<int32> Total{ 0 }; ParallelFor(Data.Num(), [&](int32 i) { Total.fetch_add(Data[i], std::memory_order_relaxed); });
**在任务仍在执行时销毁 FPipe 或 FRunnable 的所有者:**在析构函数体运行前调用 Pipe.WaitUntilEmpty() 和 Thread->Kill(true);~FPipe() 会断言 !HasWork()。
在 FRunnable::Stop() 中阻塞:Stop() 会在调用者线程上运行,而 Run() 仍在执行;只设置原子标志或触发事件,然后让 Kill(true) 等待。
在即将销毁的 actor 上使用原始委托计时器:FTimerDelegate::CreateLambda([this]{}) 会在 EndPlay 后继续调用;请使用 CreateWeakLambda/CreateUObject,并调用 ClearAllTimersForObject(this)。
相关技能
ue-cpp-foundations—TSharedPtr/TWeakObjectPtr/TStrongObjectPtr语义、GC 生命周期、子系统表ue-data-assets-tables—FStreamableManager、UAssetManager、异步资源加载和软引用ue-testing-debugging— Unreal Insights 任务和线程跟踪、stat命令、日志记录、异步代码的自动化测试ue-procedural-generation— 使用FAsyncTask/UE::Tasks执行长时间运行的生成任务、将ProceduralMeshComponent移交给游戏线程ue-mass-entity—ParallelForEachEntityChunk、EParallelExecutionFlags、处理器线程规则ue-networking-replication— RPC 和复制回调始终在游戏线程上运行ue-blueprint-cpp-interop— 将 C++ 暴露给 Blueprint:UFUNCTION/UPROPERTY 元数据键、潜在操作和异步节点ue-niagara-effects— Niagara 系统、用户参数、数据接口和数据通道ue-serialization-savegames— USaveGame、FArchive、actor 快照和配置持久化