因为游戏引擎任务间存在强依赖(如渲染须等光照、光照依赖场景加载),而std::thread和std::async不支持表达“任务A必须在B、C完成后执行”的DAG依赖关系,易导致读未初始化内存、黑块或GPU hang;需用带入度计数与CAS调度的轻量DAG任务图替代。

为什么不能直接用 std::thread 或 std::async 做引擎任务调度
因为游戏引擎里任务之间有强依赖:渲染必须等光照计算完,光照又依赖场景加载和材质解析。裸线程不记录依赖关系,std::async 默认是 fire-and-forget,无法表达“这个任务 A 必须在任务 B 和 C 都完成后才能启动”。一旦漏掉某个前置条件,就会出现读未初始化内存、纹理采样黑块、甚至 GPU hang。
真正需要的不是并发原语,而是能建模“谁等谁”的结构——DAG 就是干这个的:节点是任务,有向边表示“必须先完成”,无环保证可拓扑排序执行。
用 adjacency list + ref-count 实现轻量 DAG 节点
不用引入 Boost.Graph 或重写图算法库。每个任务封装成 TaskNode,用 std::vector<tasknode></tasknode> 存后继,用 std::atomic<int></int> 记录还有几个前置没完成(in-degree):
struct TaskNode {
std::function<void()> work;
std::vector<TaskNode*> successors;
std::atomic<int> pending_predecessors{0};
bool scheduled = false;
};
关键点:
立即学习“C++免费学习笔记(深入)”;
-
pending_predecessors初始值设为该节点所有前驱数量(构建图时算好),不是运行时动态扫描 - 前驱任务执行完后,只对每个后继调用
decrement_and_try_schedule(),避免锁或原子加减竞争 - 不存前驱指针列表——省内存,也避免循环引用和析构顺序问题
如何安全触发 DAG 执行且避免重复调度
常见错误是让多个线程同时看到 pending_predecessors == 0 然后都去调 work(),导致任务重复执行。正确做法是用 CAS 原子操作抢占调度权:
void TaskNode::decrement_and_try_schedule() {
if (--pending_predecessors == 0 && !scheduled.exchange(true)) {
work();
for (auto* next : successors) {
next->decrement_and_try_schedule();
}
}
}
注意:
-
scheduled是std::atomic<bool></bool>,防止多线程重复进入work() - 递归调用
decrement_and_try_schedule()是 OK 的——实际中深度不会太深(引擎任务图通常 - 不要在
work()里直接 new/delete 大对象;游戏帧内内存应从帧分配器(frame allocator)取,否则 GC 式分配会卡主线程
把 DAG 接入引擎主循环的时机与陷阱
不能每帧从头跑一遍拓扑排序——开销大且破坏增量性。应该只让“被标记为 dirty”的子图重调度。典型接入方式:
- 加载新关卡时,批量创建节点并连边,最后只调用入口节点(如
load_scene_root)的decrement_and_try_schedule() - 物理模拟帧间结果变化时,只重置相关节点的
pending_predecessors并重新触发(例如碰撞检测节点变 dirty,就重置其下游的响应、动画混合节点) - 绝对不要在
work()里调用vkQueueSubmit或glFlush——GPU 提交必须收束到单一线程(通常是渲染线程),任务节点只负责 CPU 计算和数据准备
DAG 本身不解决线程亲和性,你得额外用 task->thread_hint = RENDER_THREAD 这类标记,由调度器分发到对应线程池队列。



















