Job System 为什么会出现
Job System 为什么会出现
过去很多程序员学习多线程时,接触到的通常是:
std::thread
或者:
ThreadPool
然后向线程池提交任务:
pool.enqueue(task);
等待执行。
对于许多应用程序而言,这已经足够。
例如:
- 网络服务
- Web 后端
- 数据处理
- 工具软件
任务之间相对独立,执行时间也相对稳定。
但游戏引擎很快发现,问题没有这么简单。
Thread Pool 最开始工作得很好
假设一个引擎拥有:
- 物理系统
- 动画系统
- AI 系统
- 音频系统
于是很自然地写出:
pool.enqueue(physics);
pool.enqueue(animation);
pool.enqueue(ai);
pool.enqueue(audio);
交给多个线程执行。
看起来一切正常。
但随着项目规模增长,问题开始出现。
真正的问题不是线程
很多开发者会把:
线程
和:
并行
混为一谈。
实际上并不是。
例如:
Thread 0
执行 10ms
Thread 1
执行 1ms
Thread 2
执行 1ms
Thread 3
执行 1ms
虽然拥有四个线程。
但最终仍然需要等待:
10ms
才能完成这一帧。
此时三个核心已经空闲。
而一个核心仍然繁忙。
CPU 利用率并不理想。
问题并不在于线程数量。
而在于工作无法均匀分配。
CPU 核心数越来越多
早期 CPU:
2 Core
4 Core
已经足够。
而今天:
8 Core
16 Core
24 Core
32 Core
已经十分常见。
如果仍然按照:
物理线程
动画线程
渲染线程
固定划分。
那么很快就会出现:
部分核心很忙
部分核心空闲
的情况。
随着核心数量增长。
固定线程模型的利用率越来越低。
为什么不把工作拆得更细
假设:
Physics
需要更新:
10000 个刚体
那么完全可以拆分为:
Physics Job 0
Physics Job 1
Physics Job 2
...
Physics Job 31
每个 Job 负责一部分数据。
例如:
0 ~ 312
313 ~ 624
625 ~ 936
此时调度器可以根据当前负载动态分配任务。
谁空闲。
谁执行。
关注点开始发生变化。
从:
线程
变成:
工作
Job System 的核心思想
很多人认为:
Job System 是一种高级 Thread Pool。
这个说法并不完全准确。
Thread Pool 的核心对象是:
Thread
Job System 的核心对象是:
Job
开发者只负责描述:
需要完成什么工作
系统负责决定:
哪个线程执行
什么时候执行
在哪里执行
关注点从线程转移到了工作。
数据布局推动了 Job System
上一篇讨论 SIMD 时提到。
现代引擎越来越倾向于:
Position[]
Velocity[]
这样的连续数据布局。
例如:
for(size_t i = 0; i < count; i++)
{
position[i] += velocity[i];
}
这类循环天然可以拆分:
0 ~ 999
1000 ~ 1999
2000 ~ 2999
3000 ~ 3999
每个区间都是一个独立 Job。
因此:
SoA
↓
Archetype
↓
SIMD
↓
Job System
实际上是一条连续的演化路线。
连续数据不仅更适合 SIMD。
也更适合并行处理。
依赖关系开始成为问题
当 Job 数量增加之后。
新的问题出现了。
例如:
Animation
↓
Skinning
↓
Rendering
显然:
Rendering
不能在:
Skinning
之前执行。
同样:
Skinning
也不能在:
Animation
之前执行。
于是系统开始管理:
依赖关系
而不仅仅是任务队列。
此时 Job System 看起来越来越像:
调度系统
而不再只是一个线程池。
Work Stealing 的出现
大型 Job System 通常还会加入:
Work Stealing
工作窃取机制。
例如:
Core 0
很忙
Core 7
空闲
那么:
Core 7
可以主动从其他队列获取任务。
这样可以避免:
部分核心过载
部分核心空闲
的情况。
CPU 利用率进一步提高。
现代引擎中的调度器几乎都会实现类似机制。
因为真正重要的已经不是线程数量。
而是工作如何流动。
Job System 并没有消灭 Thread Pool
这是一个容易产生的误解。
实际上。
绝大多数 Job System 的底层仍然存在:
Worker Thread
区别在于:
Thread Pool 围绕线程组织系统。
Job System 围绕工作组织系统。
线程成为实现细节。
Job 成为核心抽象。
开发者思考的是:
工作之间的关系
而不是:
线程之间的关系
为什么现代引擎越来越依赖 Job System
随着硬件发展。
CPU 的提升越来越依赖:
更多核心
而不是:
更高频率
过去的问题是:
如何让一个核心更快
于是出现:
Cache
SIMD
SoA
Archetype
后来的问题变成:
如何让所有核心同时工作
于是出现:
Job System
它并不是为了替代 Thread Pool。
而是在工程规模增长后。
开发者逐渐发现:
真正需要管理的从来不是线程。
而是工作本身。
结语
从单线程到多线程。
从 Thread Pool 到 Job System。
变化的并不是底层执行单元。
CPU 仍然在执行线程。
真正发生变化的是软件的思考方式。
过去开发者思考的是:
创建多少线程
现在开发者思考的是:
如何拆分工作
如何描述依赖
如何调度任务
当核心数量不断增长时。
决定性能的往往不再是线程数。
而是工作如何流动。
而 Job System,正是这种变化后的产物。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)