DirectX12(D3D12)基础教程(九)——多线程渲染BUG修正及MsgWaitForMultipleObjects函数详解

1、前言

距离该系列文章上一篇文章已经有近半年多时间了,大家一定很好奇,为什么不继续下去了呢?说好的光追渲染去哪了呢?OK,请大家稍安勿躁,首先,光追渲染的示例正在紧张的编写当中,教程也在有条不紊的准备中,只是因为光追渲染目前在我的电脑上只能以Fallback形式运行,而征服Fallback库有点麻烦,需要些时间;

其次,在这近半年多的时间里,除了忙于工作,我主要是在按计划学习Docker、Git等方面的知识和技术,那么现在Docker基本已经学习完毕了,对我的开发帮助着实不小,大家也可以在我的博客里看到一些关于Docker的文章了,当然后面我还会继续深入的多写点关于docker的文章。毕竟Docker是当下比较热门和功能强悍的技术。Git方面呢目前只能算入门水平,还写不出个啥东西,但是我已经将一系列的文章的例子整理放到了我的Git中,欢迎大家来垂阅:https://github.com/GamebabyRockSun/GRSD3D12Sample。不得不说Git是个好东西,让我能够简单的将代码分享出来。

在整理代码的过程中,我就发现了一个隐藏在第6章多线程渲染示例代码中的深层次的BUG,我觉得很有必要给大家讲清楚,因为这个坑是我这么多年来一直没有注意到的一个问题,它也是我们彻底征服MsgWaitForMultipleObjects的最后一个问题。

2、多线程渲染示例的BUG

在整理第6章代码的时候,我有意将MsgWaitForMultipleObjects函数的超时值按照之前例子的惯例设置成了INFINITE值,这时发现画面很卡顿,有时甚至就停止渲染了,除非我用鼠标在上面晃动,它就会继续渲染并显示那个简单的动画。于是我就Trace出了函数的返回值,想看看是否有什么问题,并猜测是不是我对返回值有什么误解。结果我发现,居然连Trace都停止了,同样的,除非我不停的晃动鼠标,而返回值无论什么情况下,都是0(即代码中dwRet -= WAIT_OBJECT_0;后的dwRet值),这的确很奇怪。

经过两天的折腾(主要是上传到GitHub,并修改查找资源的路径方法之后),我突然想到一个问题,因为我们需要全部等待所有线程渲染结束(其实就是录制命令列表)后,才能执行所有命令列表,所以我们要求MsgWaitForMultipleObjects函数的fWaitAll参数为TRUE,那么是不是这里有什么猫腻?

3、MsgWaitForMultipleObjects函数详解

于是我查看了MsgWaitForMultipleObjects函数的MSDN帮助(注意第一时间找MSDN,而不是百度,因为一个网络都没有说清楚这里要描述的问题),仔细的看了下fWaitAll参数的描述,原文如下:

fWaitAll:

If this parameter is TRUE, the function returns when the states of all objects in the pHandles array have been set to signaled and an input event has been received. If this parameter is FALSE, the function returns when the state of any one of the objects is set to signaled or an input event has been received. In this case, the return value indicates the object whose state caused the function to return.

翻译后:

如果该参数为真,当pHandles数组中所有对象的状态都被设置为有信号状态并接收到输入事件时,函数返回。如果该参数为FALSE,则当将任何对象的状态设置为有信号状态或接收到输入事件时,函数返回。在此情况下,返回值指示状态导致函数返回的对象。

OK,看到我特意标粗体的说法了吗?就是说还要有消息输入,并且所有被等待的对象都是有信号状态时,这个函数才返回,两个条件必须同时成立(按:我只是想知道这种要求是怎么想出来的,为什么不能是或的关系?),而且返回的值就是WAIT_OBJECT_0。所以我们计算后dwRet值必定总是0。

因此,那么程序停止渲染,甚至卡顿的原因就很清楚了,那就是因为消息队列中并没有消息,或者更确切的说,我们并没有接受到什么输入消息,所以即使所有渲染子线程都渲染完了,也即MsgWaitForMultipleObjects函数等待的所有对象都变成了有信号状态,那也是不能返回的,同时因为我们设置了INFINITE值,那么只要你不动,那么这个画面就不动了,渲染是静止的。当然这不是我们想要的。

4、最终解决方法

此时,我们终于明白了问题的根结,那么解决就从这里开始,思路其实很简单,那就是生成一个Timer时间消息,使得消息队列中始终有消息就行了,那么修改的代码很简单,像下面这样就可以了:

SetTimer(hWnd, WM_USER + 100, 1, nullptr); //这句为了保证MsgWaitForMultipleObjects 在 wait for all 为True时 能够及时返回

//13、开始消息循环,并在其中不断渲染
while (!bExit)
{//注意这里我们调整了消息循环,将等待时间设置为0,同时将定时性的渲染,改成了每次循环都渲染
 //特别注意这次等待与之前不同
	//主线程进入等待
	dwWaitCnt = static_cast<DWORD>(arHWaited.GetCount());
	dwRet = ::MsgWaitForMultipleObjects( dwWaitCnt ,arHWaited.GetData(), TRUE,INFINITE, QS_ALLINPUT);
	dwRet -= WAIT_OBJECT_0;

一个SetTimer函数就解决了所有问题。

5、后记

最后需要澄清的就是下面这么几点,大家也可以在例子的基础上沿着这几点思路进一步去改进:

1、以使用Event对象控制1个主线程和3个渲染子线程之间的同步,是目前采取的比较保守的方案,大家可以查阅下资料试试其他的同步对象能不能完成这样的控制。提示一点:大家可以试试轻量级读写锁,看行不行,或者使用信标量;

2、以使用MsgWaitForMultipleObjects并配以INFINITE时间等待值,是因为我们目前的例子都太简单了,确实可以跑很高的帧率,但其实因为我们需要控制好几个线程间的渲染节奏,那么就不要设置为0或其他等待超时值让循环空转,这样的设置只会引起大量的无用超时值,而实际上我们没法去渲染就只能跳到下一个循环。例子中只是让子线程和主线程间交替“事件->有信号->Wait返回->渲染”循环,从而自行控制节奏,而此时INFINITE其实只是个假象,实际的循环还是以尽可能快的节奏在运转,大家可以自行计算输出帧率体会这一点,这对我们真正理解同步对象+等待函数族进行多线程同步控制的根本原理也很有帮助;

3、方面,这样的设计有助于大家调试多线程程序,一步步执行,并在暂停的情况下在不同的线程间来回切换调试,而不受等待超时,或因为过短的超时值而引起调试卡顿的情况,或干扰我们调试,这是最有利于学习和理解多线程渲染本质的一个设计;

4、目的就是让大家更进一步掌握和理解MsgWaitForMultipleObjects函数的工作原理,从而更好的掌握多线程框架的基本构建方法,从而在未来可以加入DirectInput(XInput等)、NetWork Thread Mutex等等同步对象,使得大家可以从容的应对更加复杂的多线程引擎框架的挑战;

最后要说明的是代码都已经提交到GitHub中了,大家自行下载或Clone后查看并调试运行体验一下,你可以注释SetTimer和KillTimer方法后运行看看我所说的Bug。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片