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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章