array 发表于 2008-9-22 14:24:07

OSG原创教程:最长的一帧(27)

当前位置:osgViewer/ViewerBase.cpp第264行,osgViewer:: ViewerBase::startThreading ()
我们再摘录一段第十六日中叙述的文字,这是对几个多线程渲染中重要的成员变量的解释说明:

[*]ViewerBase::_startRenderingBarrier:可以理解为渲染启动的一个栅栏标志,用于同步开始所有的图形设备的线程操作。
[*]ViewerBase::_endRenderingDispatchBarrier:渲染结束的一个栅栏标志,用于同步结束所有的图形设备的线程操作。
[*]ViewerBase::_endDynamicDrawBlock:用于同步结束所有的动态对象绘制操作,这里所谓的动态对象,指得是使用Object::setDataVariance设置为DYNAMIC的场景对象。


这里提到的栅栏(OpenThreads::Barrier),我们在第十一日中对它以及它的“孪生兄弟”Block(阻塞器)有过介绍。那么,为了方便起见,这里还是把那一段文字摘录过来:

BlockCount类:计数阻塞器类。它与阻塞器类的使用方法基本相同:block()阻塞线程,release()释放线程;不过除此之外,BlockCount的构造函数还可以设置一个阻塞计数值。计数的作用是:每当阻塞器对象的completed()函数被执行一次,计数器就减一,直至减到零就释放被阻塞的线程。

Barrier类:线程栅栏类。这是一个对于线程同步颇为重要的阻塞器接口,它的构造函数与BlockCount类似,可以设置一个整数值,我们可以把这个值理解成栅栏的“强度”。每个执行了Barrier::block()函数的线程都将被阻塞;当被阻塞在栅栏处的线程达到指定的数目时,就好比栅栏无法支撑那么大的强度一样,栅栏将被冲开,所有的线程将被释放。重要的是,这些线程是几乎同时释放的,也就保证了线程执行的同步性。

注意BlockCount与Barrier的区别,前者是由其它任意线程执行指定次数的completed()函数,即可释放被阻塞的线程;而后者则是必须阻塞指定个数的线程之后,所有的线程才会同时被释放。

为什么要长篇累牍地摘录这些内容呢?事实上,它对于我们后面的阅读有着至关重要的作用。_startRenderingBarrier和_endRenderingDispatchBarrier变量虽然是osg::BarrierOperation类型,其实均派生自OpenThreads::Barrier类;而另一个成员变量_endDynamicDrawBlock是osg::EndOfDynamicDrawBlock类型,亦即BlockCount的派生成员。因此,后文中对于这三个变量的操作,实质上也就是对于线程阻塞与同步的操作。而这正是OSG多线程渲染实现的关键所在。

osg::BarrierOperation类在定义时会传入两个参数,整型参数定义了这个栅栏可阻塞的线程数,或者说它的强度(达到这一数值时将自动释放所有线程),另一个则定义是否需要在阻塞前执行固定的操作(通常是不用的,即BarrierOperation::NO_OPERATION)。

osg::EndOfDynamicDrawBlock在定义时会传入一个参数,表示阻塞的最大计数值,当阻塞器对象的completed()函数执行次数达到这一数值时,才会释放被阻塞的线程。

下面我们就来看一下,多线程模式中,这些成员量是如何被处理的。

当前位置:osgViewer/ViewerBase.cpp第264行,osgViewer:: ViewerBase::startThreading ()
这里的switch-case条件语句段针对几种线程模型,给出了不同的预设参数,即:渲染开启栅栏_startRenderingBarrier的最大强度numThreadsOnStartBarrier,以及渲染结束栅栏_endRenderingDispatchBarrier的最大强度numThreadsOnEndBarrier。除了单线程模型之外,其余三种线程模型的设置如下。

CullDrawThreadPerContext:这一模式下,系统将为每个图形设备(GraphicsContext)创建一个线程(GraphicsContext::createGraphicsThread)。每一帧结束前都会强制同步所有的线程。栅栏的设置为:numThreadsOnStartBarrier = contexts.size()+1;
numThreadsOnEndBarrier = contexts.size()+1;DrawThreadPerContext:系统将为每个图形设备创建一个线程。并且在当前帧的所有线程完成工作之前,开始下一帧。栅栏的设置为:numThreadsOnStartBarrier = 1;
numThreadsOnEndBarrier = 1;CullThreadPerCameraDrawThreadPerContext:系统将为每个图形设备和每个摄像机创建线程(Camera::createCameraThread)。并且在当前帧的所有线程完成工作之前,开始下一帧。栅栏的设置为:numThreadsOnStartBarrier = cameras.size()+1;
numThreadsOnEndBarrier = 1;以CullDrawThreadPerContext模式为例,如果它将为各个图形设备启动共n个图形线程的话,那么“渲染启动栅栏”和“渲染结束栅栏”的强度均为n+1。这就意味着:如果每个线程在完成工作以后均使用block()函数将自己阻塞(即栅栏承受的强度将达到n),那么只要我们在主进程中再执行一次block(),就会成功地冲开栅栏,释放所有被阻塞的线程。

这就为场景渲染线程的同步提供了一种可能。那就让我们转入renderingTraversals函数探个究竟。

当前位置:osgViewer/ViewerBase.cpp第662行,osgViewer::ViewerBase::renderingTraversals()
这里将首次同步各个图形线程:if (_startRenderingBarrier.valid()) _startRenderingBarrier->block();它意味着我们选择将主进程予以阻塞,此时后面的代码都不会被执行,直到这个栅栏被冲开为止。而冲开的条件就是所有图形设备(GraphicsContext,以下简称GC)的线程都执行了_startRenderingBarrier->block()函数,并因此阻塞了运行。因此,当且仅当所有GC线程执行到block函数并被阻塞时,栅栏恰好被冲开,主进程以及各个GC线程继续执行各自后继的代码。

而在后面不远处,又有一处类似的代码段(701行):if (_endRenderingDispatchBarrier.valid()) _endRenderingDispatchBarrier->block();其作用与上一段本质上是相同的。而根据OSG中的定义,_startRenderingBarrier用于在线程渲染工作开始之前同步各个线程,而_endRenderingDispatchBarrier则在渲染工作结束之后执行同步。

注意在这两段代码之间的段落是我们之前介绍过的,用于在单线程方式下按顺序执行各个图形设备的筛选和绘制工作。概括起来就是if (!renderer->getGraphicsThreadDoesCull() && !(camera->getCameraThread()))
      renderer->cull();
if (!(gc->getGraphicsThread()) && gc->valid())
{
      makeCurrent(gc);
      gc->runOperations();
}在CullDrawThreadPerContext以及DrawThreadPerContext方式下,由于上面代码段中的“gc->getGraphicsThread()”这一条件为真,因此不再执行GraphicsContext::runOperations,也就是不再从主进程的renderingTraversals函数中执行场景绘制;而在更加高效也更加复杂的CullThreadPerCameraDrawThreadPerContext方式下,不但场景绘制不再由主进程来完成,由于“camera->getCameraThread()”这个条件也为真,因而筛选动作也是由摄像机线程来完成的。

解读成果:
渲染栅栏(Barrier)的作用。
悬疑列表:
Operation对象在线程中的应用时机是什么?

shengrendan2 发表于 2008-9-28 13:31:13

:time:

forest37 发表于 2008-11-28 10:49:38

“在CullDrawThreadPerContext以及DrawThreadPerContext方式下,由于上面代码段中的“gc->getGraphicsThread()”这一条件为真,因此不再执行GraphicsContext::runOperations,也就是不再从主进程的renderingTraversals函数中执行场景绘制;而在更加高效也更加复杂的CullThreadPerCameraDrawThreadPerContext方式下,不但场景绘制不再由主进程来完成,由于“camera->getCameraThread()”这个条件也为真,因而筛选动作也是由摄像机线程来完成的。”??
在CullDrawThreadPerContext方式下,场景筛选应当也不是由主进程完成的吧

array 发表于 2008-11-28 11:59:00

CullDrawThreadPerContext方式下确实是由GC线程完成筛选+绘制;这里我可能是将DrawThreadPerContext与CullThreadPerCameraDrawThreadPerContext作了比较,用词上有些含混,毕竟都是熬夜在写,敬请见谅。《最长的一帧》中对这三种线程模型都有图示,参看图也许更明确一些。
页: [1]
查看完整版本: OSG原创教程:最长的一帧(27)