CPU的算力是指CPU拥有的逻辑核心数(超线程下,一个物理核心等于两个逻辑核心) * 单个核心的频率,每个逻辑核心都可以挂载一个线程来实现并行计算。CPU硬件近些年的提升也是集中在可用的逻辑核心数越来越多,而非单核频率上。

举个例子,考虑一台电脑上拥有8个逻辑核心的CPU,但我们只使用单线程运算,那么程序即使写的再好也只能发挥12.5%以下的CPU性能。所以一些没有设计一套多线程基础框架的传统软件,目前非常难利用好现代CPU,虽然有一些类似Intel TBB,OpenMP之类的多线程库可以很方便地在某个功能内直接使用来优化性能,但是由于线程本身的启动,挂起,切换上下文都具备不少的性能开销,只有在明显长时间卡顿的情况下才是一个好的选择。

所以对于现代化游戏引擎或者软件设计,我们在早期就搭建好一套方便上层使用的多线程框架是有必要的:

  • 我们可以读取当前CPU硬件的核心数量,在引擎启动时便准备好固定数量的线程数,与逻辑核心挨个绑定,避免线程管理上的混乱
  • 多线程框架设计出的API可以让其他模块的开发减少对各类多线程同步原语的关注,可以按同步的编程思路实现异步,类似C#的await/async,减轻心智负担
  • 模块早期开发的逻辑和算法设计上,能注意到对多线程的使用

准备

  • 基础概念
    • 同步原语
      • Mutex
      • Spin lock
      • Atomic, lockfree
      • Semaphore
    • 协程Coroutine
    • 纤程Fiber
    • Fence/Barrier

Unity Job System用例

Job System是Unity运行时的多线程框架。可以先从它在上层使用的一些例子来简单理解。

  • 执行单个Job
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 输入Job的函数指针及需要的参数,输出Fence变量用于等待同步
    // 安排Job执行
    JobFence jobFence;
    ScheduleJob(jobFence, MyJobFunction, jobData);

    // 无须等待Job完成就可以做的一些事情
    DoSomethingNotRelatedToJob();

    // 等待Job完成
    SyncFence(jobFence);

    // 做一些依赖于Job生成数据的事情
    DoSomethingAfterFinishingJob();
  • 执行多个相互无依赖的Job
    1
    2
    3
    JobFence jobFence;
    // 与执行单个Job是类似的,换了个API,传入JobData数组以及Job的数量
    ScheduleJob(jobFence, MyJobFunction, jobDataArray, jobCount);
  • 执行两个Job,第二个Job依赖于第一个Job
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 安排第一个Job执行
    JobFence job1Fence;
    ScheduleJob(job1Fence, MyJobFunction1, myJobData);

    // 安排第二个Job执行,依赖于第一个Job完成,把Job1的Fence作为输入
    // Job1完成后会自动进行Job2
    JobFence job2Fence;
    ScheduleJobDepends(job2Fence, MyJobFunction2, myJobData, job1Fence);

    // 无须等待Job完成就可以做的一些事情
    DoSomethingNotRelatedToJob();

    // 等待第二个Job完成
    SyncFence(job2Fence);

    // 做一些依赖于Job生成数据的事情
    DoSomethingAfterFinishingJob();
  • 执行一个依赖于多个Job完成的Job
    1
    2
    3
    JobFence jobFence;
    // 与两个Job顺序执行的例子是类似的,只是需要把依赖的所有Job的Fences数组作为输入
    ScheduleJobMultipleDependencies(jobFence, MyJobFunction, jobData, dependencyFences, dependencyFenceCount);

Unity Job System的High Level设计

源码位于EditorApp项目下的Runtime/Jobs目录,我们先从high level了解一些Schedule API背后的概念。

Job是对一次函数调用的描述。

  • Job
    • JobFunc
      • typedef void JobFunc (void* userData);
    • JobData
      • void* userData;

为了存储Job的其他关联性数据,需要扩展Job,用JobInfo进行描述。

  • JobInfo
    • Job
    • JobGroup* group
    • JobInfo* next
      • 快速得到同一个JobGroup中该JobInfo的下一个JobInfo结点

上层调用ScheduleJob系列API时,第一步是把传入的单个Job或者多个Job先包装成JobInfo对象,再一起打包到给一个新创建的JobGroup作为统一的管理容器,JobGroup内部是一个JobInfo*的单向链表。

所以JobGroup创建出的数量与Schedule API调用次数是对等的。同时Schedule API需要依赖其他Job时,JobGroup会记录依赖的其他Job所在的JobGroup是什么,也就是说我们可以通过JobGroup得到一张Job System的依赖关系图。

为了让JobGroup互相之间能按照优先级,依赖关系等决定执行顺序的因素进行正确计算,需要有一个管理所有JobGroup的容器,它叫JobQueue,是一个线程安全的单例,内部存储了用于复用JobInfo,JobGroup对象的内存池,负责分配工作线程去执行Job以及工作线程的管理,相当于Job System的Context类。

JobQueue内部有两种途径去执行Job,一种是把高优先级Job压到存储优先执行Job的栈中,同是高优先级则最先Schedule的最后执行(First In Last Out);另一种是把常规Job压到一个队列中,最先Schedule的最先执行(First In First Out)。

TODO :另外Job System有一种叫做ZeroJobWorkerMode的模式,开启后JobQueue会按照高优先级的方式处理Job,暂时还没了解到它的使用场景,略。

JobFence则是用于同步某一个Job完成信号的封装类。它可以快速查询到某个Schedule API产生的Job Group的状态进行不同等待处理。比如说Job提交之后等待信号量完成Semaphore->WaitForSignal();,或者工作线程下yield出去让其他Job进来跑一会。

我们以ScheduleJob API为例做一次回顾,加深印象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void ScheduleJobInternal(JobFence& fence, JobFunc* jobFunc, void* userData, JobPriority priority)
{
// 传进来的Fence对应的JobGroup如果没初始化,那么什么都不做
// 如果初始化过了,那么需要等待该JobGroup完成,相当于调用SyncFence
BeginFence(fence);

// 拿到JobQueue单例
JobQueue& queue = GetJobQueue();

// API调用层面是把Job数据都交给JobQueue,让它去管理。
// 背后做的事情是创建一个新JobGroup,把所有JobInfo塞到JobGroup中,可以参考我下面贴的JobQueue::ScheduleJob代码。
JobGroupID defaultJobGroupID;
// 所以我们看到其返回值是JobGroupID,给JobFence做一份存根,在这之后JobFence就可以通过JobGroupID找到JobGroup进行同步等待
fence.groupID = queue.ScheduleJob(jobFunc, userData, defaultJobGroupID, GetJobQueuePriority(priority));

// 调试用的代码,略
DebugDidScheduleJob(fence, NULL, 0);
}

JobGroupID JobQueue::ScheduleJob(JobFunc* func, void* userData, JobGroupID dependency, JobQueuePriority priority)
{
// 先构造Job,再由内部包装成JobInfo,然后构造JobGroup
return ScheduleGroup(CreateJob(func, userData, dependency), priority);
}

Unity Job System的Low Level细节

TBD

Naughty Dog Fiber based Job System的设计

TBD

自制Job System的设计

  • TBD

参考链接