多线程与 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 渲染图) |
| RDG | FRHICommandListImmediate 会让 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参考
- 管线与渲染线程 · RDG 渲染图 · GC 与对象生命周期
- AnimInstance 与动画蓝图(线程安全约束) · 资源加载与异步加载
- CPU 剖析
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/e7a286e6.html
更新于:2026年10月10日