多线程与 Task Graph

自动关联目录:多线程与 Task Graph

渲染侧的线程模型见 管线与渲染线程;本篇讲通用的多线程机制与约束。

1. 定位

UE 的线程是有明确分工的,不是"随便开个线程干活":每个线程有自己的所有权规则,越界就是崩溃或数据竞争。

一句话定位:写 UE 多线程代码,先问"这段代码属于哪个线程的职责",而不是"能不能并行"。

2. 主要线程

线程职责
Game Thread游戏逻辑、UObject、Actor、蓝图
Render Thread渲染命令录制(见 管线与渲染线程)
RHI Thread提交到底层 API
Task Graph 工作线程通用并行任务
Async Loading Thread资源加载(见 资源加载)
音频线程等各子系统自有线程

铁律:UObject / Actor 只能在 Game Thread 访问。这不是建议,是硬性约束(GC 与 UObject 系统不是线程安全的,见 GC)。

3. Task Graph

// 在工作线程上跑一个任务
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, []()
{
    // 纯计算,不碰 UObject
});

// TGraphTask:带依赖的图任务
FGraphEventRef Task = TGraphTask<FMyTask>::CreateTask().ConstructAndDispatchWhenReady(Params);
机制用途
AsyncTask(NamedThread, Lambda)最简单的异步
TGraphTask<>带前置依赖的任务图
ParallelFor并行循环
Async(EAsyncExecution::ThreadPool, ...)返回 TFuture,可取结果
FFunctionGraphTask::CreateAndDispatchWhenReady函数式任务

命名线程(ENamedThreads):GameThread / ActualRenderingThread / AnyBackgroundThreadNormalTask / AnyHiPriThreadHiPriTask 等,用来声明任务该跑在哪类线程上。

4. 跨线程通信

机制用途
AsyncTask(ENamedThreads::GameThread, ...)把结果送回游戏线程
ENQUEUE_RENDER_COMMAND送命令给渲染线程(见管线篇)
TCriticalSection / FRWLock锁
std::atomic / TAtomic原子
TQueue / TCircularQueue无锁队列
FPromise / TFuture异步结果

最常见的正确姿势:工作线程只做纯计算 → 结果通过 AsyncTask(GameThread, ...) 回到游戏线程再碰 UObject。

5. 各系统的线程约束(散在各篇)

系统约束
动画NativeThreadSafeUpdateAnimation 在工作线程,不能访问 UObject(见 AnimBP)
渲染pass lambda 不能有副作用(见 RDG 渲染图)
RDGFRHICommandListImmediate 会让 pass 失去并行资格
物理遮挡/射线检测的成本与线程
GC只在游戏线程

6. 代价与权衡

设计收益代价什么时候不该用
Task Graph自动负载均衡任务太小则调度开销占比高极小的任务直接同步跑
工作线程做纯计算显著提升结果要送回游戏线程需要频繁访问 UObject 时没法并行
ParallelFor简单并行要注意数据竞争循环体有副作用时不可用
锁保护共享数据竞争时反而更慢能无锁就无锁
开自己的 std::thread完全控制绕开引擎的线程管理一般不该用,用 Task Graph

7. 踩坑与排查

坑现象怎么验证
工作线程访问 UObject随机崩溃/数据竞争检查工作线程代码里有没有碰 UObject
结果直接在工作线程改 Actor崩溃用 AsyncTask(GameThread, ...)
任务太小调度开销大于收益批量合并
死锁卡住检查锁顺序;避免在持锁时调回游戏线程
用 std::thread 而非 Task Graph与引擎线程模型脱节改用引擎机制
忘了同步就读结果读到半截数据用 TFuture / FGraphEvent 等待
在渲染线程做阻塞等待卡渲染见 管线与渲染线程

8. 排查顺序

1. 崩溃在工作线程?→ 是否访问了 UObject
2. 结果不对?→ 是否等待了任务完成;有没有数据竞争
3. 卡住?→ 死锁(锁顺序 / 持锁回调)
4. 没加速?→ 任务粒度是否太小;锁竞争
5. 用对机制了吗?→ 优先 Task Graph / ParallelFor,少用裸 thread

参考