一种并行化游戏引擎的设计方案

来源:互联网 发布:淘宝上的小米手环2 编辑:程序博客网 时间:2024/06/05 20:51

需要解决的问题:

目前游戏一般都是单线程的,只是在部分系统例如骨骼动画,粒子系统等在数据上运算很独立的部分才使用多线程来执行。或者将系统拆分成几个可以并行的模块进行并行,例如将渲染单独放到一个线程中。但这些解决方法都无法根据CPU数目的继续增长而提高。例如把渲染单独提出来作为一个线程可以有效的利用双核系统,但对于更多的核则无法利用。我们希望能找到一种方法,能够有效的适应CPU数目的增长,并且能足够平均的分配各个CPU的工作量的一种方法。

 

任务分解和依赖关系:这个方案的基本思想是将游戏中每一帧的所有工作拆分成一个个足够小的任务,并设置好这些任务之间的依赖关系。这些小任务会形成一个有向无环的依赖图,然后利用一个任务调度器根据依赖关系调度这些任务,把它们按照顺序放到线程池中执行。
例如下图的一组任务,它们的依赖关系如图所示:

依赖图与线程池

 

这些任务会先执行0,当0执行完成后,1跟2就可以并行执行,以此类推。线程池用于保证实际执行的线程等于CPU的数目,以避免CPU线程切换的开销。
任务排序的算法:
这样的任务调度算法类似于图论中的拓扑排序,但在这里并不准备使用邻接矩阵来实现。因为邻接矩阵的空间复杂度是n*n,对于任务的细分解不利。并且也无法动态的添加和删除任务。
因此这里我们为每一个任务保存一个依赖任务的列表(叫dependentTaskList)和被依赖任务的列表(叫beDependedTaskList),以及保存一个还未解决任务的数目(unresoveledTaskCnt)。大致结构如下:

共享资源的问题:
这个模型还有一个问题就是可能存在一组任务共享同一资源,这样这一组任务中同一时间就只能有一个任务执行。比如所有的渲染任务,由于它们都共享一个渲染设备,因此它们之间只能并行的执行。
对于这个问题我们可以使用分组和等待队列来完成。将共同使用同一资源的任务安排到一组,并为这一组分配一个分组队列。在任务调度器进行任务调度的时候,如果任务没有分组,则直接放入线程池的等待任务队列,否则就检查该分组的任务队列中是否有任务,如果没有则将自己同时放到线程池的等待任务队列和分组的队列上去。
当线程池每完成一个任务将通知调度器,然后调度器检查这个完成的任务是否存在分组。如果存在分组,则该任务一定是分组队列列首的任务。这时则把该任务从分组队列中pop出来,并将分组中队列中的下一个任务放到线程池中去。

 

这样看起来能够并行的操作并不多,但只要在游戏中我们将任务分解得足够的细,就可以达到足够的并行度。例如游戏引擎中场景图和GUI的更新,渲染任务。它们本身就是一颗树,那么则可以将这棵树上的每一个节点的更新和渲染封装成一个任务。

具体算法:
假设所有任务都连接到一个单点的启始任务上。我们从这个单点上执行这个算法:
a)         将所有任务图中的m_unresoveledTaskCnt(还未解决的依赖任务数)修改成该任务所依赖任务的数量;
b)   执行起点的单点任务;
c)   遍历刚才解决任务的m_beDependedTaskList(所有依赖于它的任务列表),将它们的m_unresoveledTaskCnt都减1;如果这些任务中有m_unresoveledTaskCnt等于0的:
              i.      判断该任务是否存在分组,如果存在分组:
1.   如果分组队列为空,则将任务放入线程池中执行;
2.   如果不为空,则压如队列中(保证同一分组始终只有一个在执行);
            ii.      如果该任务没有分组,则直接放入线程池中执行;
d)   记录下刚才放入线程池中的任务数量,并在线程池上等待这些任务的完成;
              i.      判断该完成的任务是否存在分组,如果存在分组:
1.   如果分组不为空,则将分组队列中下一个任务取出来放入线程池中;
递归回到c)处执行。

 

这个方案感觉还是对任务划分的粒度上控制很困难.如果任务划分得太细,对于依赖关系的确定比较困难.而且由于每个任务进入线程池的时候都会触发一次信号量的操作,如果太多的任务也会降低整个系统的性能.另一方面如果任务划分得过粗,则得不到很好的并行性.

 

只是一个想法而已.