任务系统
Tasks System
UE::Tasks 是 UE5 的任务级并行库,是对 TaskGraph 的改进,与 TaskGraph 共用同一套调度器和工作线程。它支持启动任务、等待或取结果、声明前置依赖、嵌套任务、管道 (Pipe) 和任务事件。官方建议用依赖关系把工作连成图,而不是阻塞等待;5.5 起用「超额订阅」取代了容易死锁的忙等待。
原理
官方:Tasks System 改进自 UE 原有的作业管理器 TaskGraph,两者使用同一个后端(调度器与工作线程)。主要功能:(1) 提供可调用对象来异步启动任务;(2) 等待任务完成或取得结果;(3) 指定前置任务 (prerequisites);(4) 在任务内部启动嵌套任务,所有嵌套任务完成后父任务才算完成;(5) 构建任务链,即管道 (pipe);(6) 用任务事件 (task event) 做同步和通知。(Tasks Systems in Unreal Engine)这是 多线程渲染与任务系统 中「任务图」思想在 UE 里的实现。
flowchart LR
A["任务 A:加载数据"] --> C["任务 C:合并结果"]
B["任务 B:计算"] --> C
C --> D["任务 D:提交给游戏线程"]
subgraph P["管道 Pipe:串行访问共享资源"]
P1["写入 1"] --> P2["写入 2"]
end图中 A、B 是 C 的前置任务,C 等两者都完成后才开始;下方的管道中,任务按顺序一个接一个执行,从而无需加锁就能串行访问同一个资源。
用法要点
- 启动任务时要给一个调试名(用于定位是哪段代码启动的任务)和任务体。可以「发射后不管」,也可以通过优先级影响执行顺序(高优先级先执行)。启动即开始任务的生命周期并分配资源。(Tasks Systems in Unreal Engine)
- 少用阻塞等待:
Wait和GetResult()会阻塞调用线程,直到任务完成;官方建议「通过定义任务之间的依赖关系来构建任务图,并设计基于任务的异步 API」。依赖的主要好处是不阻塞工作线程。(Tasks Systems in Unreal Engine) - 管道代替互斥锁:传统做法是对共享资源加锁,线程会被阻塞,资源争用时性能代价很大。管道是一串依次执行(不并发)的任务;官方设计意图是每个共享资源一个管道,对它的所有访问都通过该管道启动的任务进行。(Tasks Systems in Unreal Engine)
坑:忙等待与超额订阅
5.5 起废弃忙等待 (busy-waiting)。以前忙等待会在等待时执行别的任务,但它可能挑到一个反过来依赖当前等待任务的任务,在同一个调用栈上形成无法解开的循环依赖,经常导致死锁;挑到长任务时会卡顿,还容易栈溢出。5.5 改用超额订阅 (oversubscription):等待时唤醒额外的线程,代价是多用一点内存。大多数等待函数已经内置了超额订阅作用域,程序员无需额外处理。这些额外线程称为 standby 线程,会在 Unreal Insights 中显示。(Tasks Systems in Unreal Engine,Insights 见 UE5 性能分析)
与 GEA 的对照(卷 I 第 4 章)
- 管道 vs 锁:官方说互斥锁会阻塞线程、争用时代价大,建议用管道串行访问共享资源。GEA 给出了底层原因:内核同步原语每次都要内核调用,上下文切换可超过 1000 个时钟周期(线程同步原语),并主张尽量减少争用和锁的使用(基于锁的并发问题)。管道其实就是 GEA 所说「把操作串行化」的一种任务级实现。
- 忙等待死锁:官方描述的「等待时执行别的任务,却挑到反过来依赖当前任务的任务」正是 GEA 的 Coffman「循环等待」条件。GEA 4.4.7 提到可以用纤程实现协作式的任务系统,并警告纤程里不能调用阻塞的系统调用;UE 5.5 选择的是另一条路——唤醒额外线程(超额订阅)(进程、线程与纤程)。两者都是为了在「等待」时不让工作线程空转或死锁,做法不同,本页不判断优劣。
出处
- Tasks Systems in Unreal Engine UE 5.8 文档
- Game_Engine_Architecture_4Ed_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 I 第 4 章 4.4.7、4.6、4.7 节