mcpField
U
ue-async-threading

ue-async-threading

github.com/quodsoler开发4191.1k 次安装4 周前更新
代码仓库

关于 ue-async-threading

面向 UE 5.8 开发者的异步与线程技能,帮助选择后台任务、并行计算和线程同步方案。汇总 UE::Tasks、Async、线程池、专用线程以及计时器等 API,并说明 UObject 的线程规则。

主要功能

  1. 01UE::Tasks 与任务依赖
  2. 02Async、TFuture 与游戏线程调度
  3. 03线程池任务和 ParallelFor
  4. 04FRunnable 专用线程
  5. 05同步原语、Ticker 与 Timer

适用场景

  1. 01将计算任务移出游戏线程
  2. 02并行处理网格或批量数据
  3. 03按顺序执行共享资源任务
  4. 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::LaunchTTask<T>
从任意位置在游戏线程上运行 lambdaAsyncTask(ENamedThreads::GameThread, ...)无
可选择执行上下文的 Future 风格结果Async(EAsyncExecution, ...)TFuture<T>
串行执行访问同一资源的任务(FIFO)UE::Tasks::FPipeTTask<T>
限制同时运行的任务数量UE::Tasks::FTaskConcurrencyLimiter无
由所有者管理生命周期的可复用池化工作单元FAsyncTask<T> / FAutoDeleteAsyncTask<T>通过 GetTask()
数据并行循环,调用方阻塞等待ParallelFor无
长期运行的线程(套接字、文件监视器、模拟循环)FRunnable + FRunnableThread::Create手动管理
Actor Tick 之外的每帧回调FTSTicker::GetCoreTicker().AddTicker句柄
在游戏线程上延迟调用或重复调用FTimerManager::SetTimerFTimerHandle

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
ThreadPoolGThreadPool
LargeThreadPoolGLargeThreadPool,仅限 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);在作用域内释放时使用 FScopeUnlockHAL/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::FMutexUE::TUniqueLock<UE::FMutex>;使用 UE::DeferLock 的 UE::TDynamicUniqueLockAsync/Mutex.h:18、Async/UniqueLock.h:19,48、Async/LockTags.h:12
小型递归互斥锁UE::FRecursiveMutexUE::TUniqueLockAsync/RecursiveMutex.h:19
小型读写锁UE::FSharedMutex(LockShared/UnlockShared)UE::TSharedLock、UE::TUniqueLockAsync/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。

线程安全规则

  1. **仅限游戏线程:**任何 UPROPERTY 读写、UFUNCTION 调用、GetWorld()、生成、销毁、组件变更、UObject 上的委托、计时器、ticker。使用 check(IsInGameThread()) 为入口添加保护。
  2. **绝不要将原始 UObject* 或 this 捕获到延迟工作中。**捕获 TWeakObjectPtr<T>(UObject/WeakObjectPtrTemplates.h:25),并在游戏线程上通过 Get() 解析,或使用 CreateWeakLambda 构建委托。
  3. GC 可能在启动任务和回调之间运行。FGCScopeGuard(UObject/GarbageCollection.h:117)会在作用域内阻止 GC;仅将其用于工作线程上的短时只读访问,绝不要在阻塞等待期间使用。
  4. 渲染线程:ENQUEUE_RENDER_COMMAND(MyCommand)([Data](FRHICommandListImmediate& RHICmdList) { UploadOnRenderThread(RHICmdList, Data); });(RenderCore/Public/RenderingThread.h:1087,模块 RenderCore);从游戏线程调用 FlushRenderingCommands() 可排空渲染命令。使用 IsInRenderingThread() 检查。
  5. 共享数据需要自己的锁,即使它位于线程安全的 TSharedPtr 中也不例外;引用计数是原子的,但负载不是。
  6. 优先使用无锁交接: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)
FThreadSafeCounterstd::atomic<int32>头文件注释为“已弃用。请使用 std::atomic<int32>”(HAL/ThreadSafeCounter.h:9)
FThreadSafeBoolstd::atomic<bool>头文件注释为“已弃用”(HAL/ThreadSafeBool.h:9)
TAtomic<T>std::atomic<T>“计划弃用”(Templates/Atomic.h:13、:528);尚无 UE_DEPRECATED 宏
FExternalMutexTIntrusiveMutex<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::SpscTMpscQueue<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::ThreadSafeTemplates/SharedPointerFwd.h:25
新任务使用 TGraphTask<T> / FGraphEventRefUE::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 快照和配置持久化

评论

支持 Markdown