《Exploring in UE4》多线程机制详解[原理分析]
目錄
一.概述
二."標(biāo)準(zhǔn)"多線程
三.AsyncTask系統(tǒng)
3.1 FQueuedThreadPool線程池
3.2 Asyntask與IQueuedWork
3.3 其他相關(guān)技術(shù)細(xì)節(jié)
四.TaskGraph系統(tǒng)
4.1 從Tick函數(shù)談起
4.2 TaskGraph系統(tǒng)中的任務(wù)與線程
4.3 TaskGraph系統(tǒng)中的任務(wù)與事件
4.4 其他相關(guān)技術(shù)細(xì)節(jié)
五.總結(jié)
一.概述
多線程是優(yōu)化項(xiàng)目性能的重要方式之一,游戲也不例外。雖然經(jīng)常能看到“游戲不適合利用多線程優(yōu)化”的言論,但我個(gè)人覺得這句話更多的是針對(duì)GamePlay,游戲中多線程用的一點(diǎn)也不少,比如渲染模塊、物理模塊、網(wǎng)絡(luò)通信、音頻系統(tǒng)、IO等。下圖就展示了UE4引擎運(yùn)行時(shí)的部分線程,可能比你想象的還要多一些。
?
雖然UE4遵循C++11的標(biāo)準(zhǔn),但是他并沒有使用std::thread,而是自己實(shí)現(xiàn)了一套多線程機(jī)制(應(yīng)該是從UE3時(shí)代就有了,未考證),用法上很像Java。當(dāng)然,你如果想用std::thread也是完全沒有問題的。
在UE4里面,我們可以自己繼承FRunnable接口創(chuàng)建單個(gè)線程,也可以直接創(chuàng)建AsyncTask來調(diào)用線程池里面空閑的線程,還可以通過TaskGraph系統(tǒng)來異步完成一些自定義任務(wù)。雖然本質(zhì)相同,但是用法不同,理解上也要花費(fèi)不少時(shí)間,這篇文章會(huì)對(duì)里面的各個(gè)機(jī)制逐個(gè)分析并做出總結(jié),但并不會(huì)深入討論線程的實(shí)現(xiàn)原理、線程安全等內(nèi)容。另外,由于個(gè)人接觸多線程編程的時(shí)間不長,有一些內(nèi)容可能不是很準(zhǔn)確,歡迎大家一起討論。
二.“標(biāo)準(zhǔn)”多線程
我們先從最基本的創(chuàng)建方式談起,這里的“標(biāo)準(zhǔn)”只是一個(gè)修飾。其實(shí)就是創(chuàng)建一個(gè)繼承自FRunnable的類,把這個(gè)類要執(zhí)行的任務(wù)分發(fā)給其他線程去執(zhí)行。FRunnable就是一個(gè)很簡單的類,里面只有5,6個(gè)函數(shù)接口,為了與真正的線程區(qū)分,我這里稱FRunnable為“線程執(zhí)行體”。
?
看起來這么簡單個(gè)類,我們是不是可以不繼承他,單獨(dú)寫一個(gè)類再把這幾個(gè)接口放進(jìn)去呢?當(dāng)然不行,實(shí)際上,在實(shí)現(xiàn)多線程的時(shí)候,我們需要將FRunnable作為參數(shù)傳遞到真正的線程里面,然后才能通過線程去調(diào)用FRunnable的Run,也就是我們具體實(shí)現(xiàn)的類的Run方法(通過虛函數(shù)覆蓋父類的Run)。所謂真正的線程其實(shí)就是FRunnableThread,不同平臺(tái)的線程都繼承自他,如FRunnableThreadWin,里面會(huì)調(diào)用Windows平臺(tái)的創(chuàng)建線程的API接口。下圖給出了FRunnable與線程之間的關(guān)系類圖:
?
在實(shí)現(xiàn)的時(shí)候,你需要繼承FRunnable并重寫他的那幾個(gè)函數(shù),Run()里面表示你在線程里面想要執(zhí)行的邏輯。具體的實(shí)現(xiàn)方式網(wǎng)上有很多案例,這里給出UE4Wiki的教程鏈接:
Multi-Threading: How to Create Threads in UE4
三.AsyncTask系統(tǒng)
說完了UE4“標(biāo)準(zhǔn)”線程的使用,下面我們來談?wù)勆晕?fù)雜一點(diǎn)的AsyncTask系統(tǒng)。AsyncTask系統(tǒng)是一套基于線程池的異步任務(wù)處理系統(tǒng)。如果你沒有接觸過UE4多線程,用搜索引擎搜索UE4多線程時(shí)可能就會(huì)看到類似下面這樣的用法。
?
沒錯(cuò),這就是官方代碼里面給出的一種異步處理的解決方案示例。不過你可能更在意的是這個(gè)所謂多線程的用法,看起來非常簡單,但是卻找不到任何帶有“Thread”或“Runnable”的字樣,那么他也是用Runnable的方式做的么?答案肯定是Yes。只不過封裝的比較深,需要我們深入源碼才能明白其中的原理。
?
3.1 FQueuedThreadPool線程池
在介紹AsynTask之前先講一下UE里面的線程池,FQueuedThreadPool。和一般的線程池實(shí)現(xiàn)類似,線程池里面維護(hù)了多個(gè)線程FQueuedThread與多個(gè)任務(wù)隊(duì)列IQueuedWork,線程是按照隊(duì)列的方式來排列的。在引擎PreInit的時(shí)候執(zhí)行相關(guān)的初始化操作,代碼如下
?
這段代碼我們可以看出,專有服務(wù)器的線程池GThreadPool默認(rèn)只開一個(gè)線程,非專有服務(wù)器的根據(jù)核數(shù)開(CoreNum-1)個(gè)線程。編輯器模式會(huì)另外再創(chuàng)建一個(gè)線程池GLargeThreadPool,包含(LogicalCoreNum-2)個(gè)線程,用來處理貼圖的壓縮和編碼相關(guān)內(nèi)容。
在線程池里面所有的線程都是FQueuedThread類型,不過更確切的說FQueuedThread是繼承自FRunnable的線程執(zhí)行體,每個(gè)FQueuedThread里面包含一個(gè)FRunnableThread作為內(nèi)部成員。
相比一般的線程,FQueuedThread里面多了一個(gè)成員FEvent* DoWorkEvent,也就是說FQueuedThread里面是有一個(gè)事件觸發(fā)機(jī)制的。那么這個(gè)事件機(jī)制的作用是什么?二手手游拍賣平臺(tái)一般情況下來說,就是在沒有任務(wù)的時(shí)候掛起這個(gè)線程,在添加并分配給該線程任務(wù)的時(shí)候激活他,不過你可以靈活運(yùn)用它,在你需要的時(shí)候去動(dòng)態(tài)控制線程任務(wù)的執(zhí)行與暫停。前面我們?cè)诮o線程池初始化的時(shí)候,通過FQueuedThreadPool的Create函數(shù)創(chuàng)建了多個(gè)FQueuedThread,然后每個(gè)FQueuedThread會(huì)執(zhí)行Run函數(shù),里面有一段邏輯如下:
我們看到,當(dāng)DoWorkEvent執(zhí)行Wait的時(shí)候,如果該線程的Event處于無信號(hào)狀態(tài)(默認(rèn)剛創(chuàng)建是無信號(hào)的),那么wait會(huì)等待10毫秒并返回false,線程處于While無限循環(huán)中。如果線程池添加了任務(wù)(AddQueuedWork)并執(zhí)行了DoWorkEvent的Trigger函數(shù),那么Event就會(huì)被設(shè)置為有信號(hào),Wait函數(shù)就會(huì)返回true,隨后線程跳出循環(huán)進(jìn)而處理任務(wù)。
?
目前我們接觸的類之間的關(guān)系如下圖:
?
3.2 Asyntask與IQueuedWork
線程池的任務(wù)IQueuedWork本身是一個(gè)接口,所以得有具體實(shí)現(xiàn)。這里你就應(yīng)該能猜到,所謂的AsynTask其實(shí)就是對(duì)IQueuedWork的具體實(shí)現(xiàn)。這里AsynTask泛指FAsyncTask與FAutoDeleteAsyncTask兩個(gè)類,我們先從FAsyncTask說起。
FAsyncTask有幾個(gè)特點(diǎn),
?
- FAsyncTask是一個(gè)模板類,真正的AsyncTask需要你自己寫。通過DoWork提供你要執(zhí)行的具體任務(wù),然后把你的類作為模板參數(shù)傳過去
- 使用FAsyncTask就默認(rèn)你要使用UE提供的線程池FQueuedThreadPool,前面代碼里說明了在引擎PreInit的時(shí)候會(huì)初始化線程池并返回一個(gè)指針GThreadPool。在執(zhí)行FAsyncTask任務(wù)時(shí),如果你在執(zhí)行StartBackgroundTask的時(shí)候會(huì)默認(rèn)使用GThreadPool線程池,當(dāng)然你也可以在參數(shù)里面指定自己創(chuàng)建的線程池
- 創(chuàng)建FAsyncTask并不一定要使用新的線程,你可以調(diào)用函數(shù)StartSynchronousTask直接在當(dāng)前線程上執(zhí)行任務(wù)
- FAsyncTask本身包含一個(gè)DoneEvent,任務(wù)執(zhí)行完成的時(shí)候會(huì)激活該事件。當(dāng)你想等待一個(gè)任務(wù)完成時(shí)再做其他操作,就可以調(diào)用EnsureCompletion函數(shù),他可以從隊(duì)列里面取出來還沒被執(zhí)行的任務(wù)放到當(dāng)前線程來做,也可以掛起當(dāng)前線程等待DoneEvent激活后再往下執(zhí)行
FAutoDeleteAsyncTask與FAsyncTask是相似的,但是有一些差異,
?
- 默認(rèn)使用UE提供的線程池FQueuedThreadPool,無法使用其他線程池
- FAutoDeleteAsyncTask在任務(wù)完成后會(huì)通過線程池的Destroy函數(shù)刪除自身或者在執(zhí)行DoWork后刪除自身,而FAsyncTask需要手動(dòng)delete
- 包含F(xiàn)AsyncTask的特點(diǎn)1和特點(diǎn)3
總的來說,AsyncTask系統(tǒng)實(shí)現(xiàn)的多線程與你自己字節(jié)繼承FRunnable實(shí)現(xiàn)的原理相似,不過他在用法上比較簡單,而且還可以直接借用UE4提供的線程池,很方便。
最后我們?cè)賮硎崂硪幌逻@些類之間的關(guān)系:
?
3.3 其他相關(guān)技術(shù)細(xì)節(jié)
大家在看源碼的時(shí)候可能會(huì)遇到一些疑問,這里簡單列舉并解釋一下
1. FScopeLock
FScopeLock是UE提供的一種基于作用域的鎖,思想類似RAII機(jī)制。在構(gòu)造時(shí)對(duì)當(dāng)前區(qū)域加鎖,離開作用域時(shí)執(zhí)行析構(gòu)并解鎖。UE里面有很多帶有“Scope”關(guān)鍵字的類,如移動(dòng)組件中的FScopedMovementUpdate,Task系統(tǒng)中的FScopeCycleCounter,FScopedEvent等,他們的實(shí)現(xiàn)思路是類似的。
2. FNonAbandonableTask
繼承FNonAbandonableTask的Task不可以在執(zhí)行階段終止,即使執(zhí)行Abandon函數(shù)也會(huì)去觸發(fā)DoWork函數(shù)。
?
3.AsyncTask與轉(zhuǎn)發(fā)構(gòu)造
通過本章節(jié)開始的例子,我們知道創(chuàng)建自定義任務(wù)的方式如下
FAsyncTask<ExampleAsyncTask>*MyTask= new FAsyncTask<ExampleAsyncTask>(5);
括號(hào)里面的5會(huì)以參數(shù)轉(zhuǎn)發(fā)的方式傳到的ExampleAsyncTask構(gòu)造函數(shù)里面,這一步涉及到C++11的右值引用與轉(zhuǎn)發(fā)構(gòu)造,具體細(xì)節(jié)可以去網(wǎng)上搜索一下。
?
四.TaskGraph系統(tǒng)
說完了FAsyncTask系統(tǒng),接下來我們?cè)僬務(wù)劯鼜?fù)雜的TaskGraph系統(tǒng)(應(yīng)該不會(huì)有比他更復(fù)雜的了)。Task Graph 系統(tǒng)是UE4一套抽象的異步任務(wù)處理系統(tǒng),可以創(chuàng)建多個(gè)多線程任務(wù),指定各個(gè)任務(wù)之間的依賴關(guān)系,按照該關(guān)系來依次處理任務(wù)。具體的實(shí)現(xiàn)方式網(wǎng)上也有很多案例,這里先給出UE4Wiki的教程鏈接:
Multi-Threading: Task Graph System
建議大家先了解其用法,然后再往下閱讀。
4.1 從Tick函數(shù)談起
平時(shí)調(diào)試的時(shí)候,我們隨便找個(gè)Tick斷點(diǎn)一下都能看到類似下圖這樣的函數(shù)堆棧。如果你前面的章節(jié)都看懂的話,這個(gè)堆棧也能大概理解。World在執(zhí)行Tick的時(shí)候,觸發(fā)了FNamedTaskThread線程去執(zhí)行任務(wù)(FTickFunctionTask),任務(wù)FTickFunctionTask具體的工作內(nèi)容就是執(zhí)行ACtorComponent的Tick函數(shù)。其實(shí),這個(gè)堆棧也說明了所有Actor與Component的Tick都是通過TaskGraph系統(tǒng)來執(zhí)行的。
?
不過你可能還是會(huì)有很多問題,TaskGraph斷點(diǎn)為什么是在主線程里面?FNamedTaskThread是什么意思?FTickFunctionTask到底是在哪個(gè)線程執(zhí)行?答案在下一小節(jié)逐步給出。
4.2 TaskGraph系統(tǒng)中的任務(wù)與線程
既然是Task系統(tǒng),那么應(yīng)該能猜到他和前面的AsyncTask系統(tǒng)相似,我們可以創(chuàng)建多個(gè)Task任務(wù)然后分配給不同的線程去執(zhí)行。在TaskGraph系統(tǒng)里面,任務(wù)類也是我們自己創(chuàng)建的,如FTickFunctionTask、FReturnGraphTask等,里面需要聲明DoTask函數(shù)來表示要執(zhí)行的任務(wù)內(nèi)容,GetDesiredThread函數(shù)來表示要在哪個(gè)線程上面執(zhí)行,大概的樣子如下:
?
而線程在該系統(tǒng)里面稱為FWorkerThread,通過全局的單例類FTaskGraphImplementation來控制創(chuàng)建和分配任務(wù)的,默認(rèn)情況下會(huì)開啟5個(gè)基本線程,額外線程的數(shù)量則由下面的函數(shù)NumberOfWorkerThreadsToSpawn來決定,FTaskGraphImplementation的初始化在FEngineLoop.PreInit里面進(jìn)行。當(dāng)然如果平臺(tái)本身不支持多線程,那么其他的工作也會(huì)在GameThread里面進(jìn)行。
?
前面提到的FWorkerThread雖然可以理解為工作線程,但其實(shí)他不是真正的線程。FWorkerThread里面有兩個(gè)重要成員,一個(gè)是FRunnableThread* RunnableThread,也就是真正的線程。另一個(gè)是FTaskThreadBase* TaskGraphWorker,即繼承自FRunnable的線程執(zhí)行體。FTaskThreadBase有兩個(gè)子類,FTaskThreadAnyThread和FNamedTaskThread,分別表示非指定名稱的任意Task線程執(zhí)行體和有名字的Task線程執(zhí)行體。我們平時(shí)說的渲染線程、游戲線程就是有名稱的Task線程,而那些我們創(chuàng)建后還沒有使用到的線程就是非指定名稱的任意線程。
?
在引擎初始化FTaskGraphImplementation的時(shí)候,我們就會(huì)默認(rèn)構(gòu)建24個(gè)FWorkerThread工作線程(這里支持最大的線程數(shù)量也就是24),其中里面有5個(gè)是默認(rèn)帶名字的線程,StatThread、RHIThread、AudioThread、GameThread、ActualRenderingThread,還有前面提到的N個(gè)非指定名稱的任意線程,這個(gè)N由CPU核數(shù)決定。對(duì)于帶有名字的線程,他不需要?jiǎng)?chuàng)建新的Runnable線程,因?yàn)樗麄儠?huì)在其他的時(shí)機(jī)創(chuàng)建,如StatThread以及RenderingThread會(huì)在FEngineLoop.PreInit里創(chuàng)建。而那N個(gè)非指定名稱的任意線程,則需要在一開始就手動(dòng)創(chuàng)建Runnable線程,同時(shí)設(shè)置其優(yōu)先級(jí)比前面線程的優(yōu)先級(jí)要低。到這里,我們應(yīng)該可以理解,有名字的線程專門要做他名字對(duì)應(yīng)的事情,非指定名稱的任意線程則可以用來處理其他的工作,我們?cè)贑reateTask創(chuàng)建任務(wù)時(shí)會(huì)通過自己寫好的函數(shù)決定當(dāng)前任務(wù)應(yīng)該在哪個(gè)線程執(zhí)行。
?
現(xiàn)在我們可以先回答一下上一節(jié)的問題了,FTickFunctionTask到底是在哪個(gè)線程執(zhí)行?答案是游戲主線程,我們可以看到FTickFunctionTask的Desired線程是Context.Thread,而Context.Thread是在下圖賦值的,具體細(xì)節(jié)參考FTickTaskManager與FTickTaskLevel的使用。
?
這里我們?cè)偎伎家幌?#xff0c;如果我們將多個(gè)任務(wù)投放到一個(gè)線程那么他們是按照什么順序執(zhí)行的呢?這個(gè)答案需要分兩種情況解答,對(duì)于投放到FTaskThreadAnyThread執(zhí)行的任務(wù)會(huì)在創(chuàng)建的時(shí)候按照優(yōu)先級(jí)放到IncomingAnyThreadTasks數(shù)組里面,然后每次線程完成任務(wù)后會(huì)從這個(gè)數(shù)組里面彈出未執(zhí)行的任務(wù)來執(zhí)行,他的特點(diǎn)是我們有權(quán)利隨時(shí)修改和調(diào)整這個(gè)任務(wù)隊(duì)列。而對(duì)于投放到FNamedTaskThread執(zhí)行的任務(wù),會(huì)被放到其本身維護(hù)的隊(duì)列里面,通過FThreadTaskQueue來處理執(zhí)行順序,一旦放到這個(gè)隊(duì)列里面,我們就無法隨意調(diào)整任務(wù)了。
?
4.3 TaskGraph系統(tǒng)中的任務(wù)與事件
雖然前面已經(jīng)比較細(xì)致的描述了TaskGraph系統(tǒng)的框架,但是一個(gè)非常重要的特性我們還沒講到,就是任務(wù)依賴的實(shí)現(xiàn)原理。怎么理解任務(wù)依賴呢?簡單來說,就是一個(gè)任務(wù)的執(zhí)行可能依賴于多個(gè)事件對(duì)象,這些事件對(duì)象都觸發(fā)之后才會(huì)執(zhí)行這個(gè)任務(wù)。而這個(gè)任務(wù)完成后,又可能觸發(fā)其他事件,其他事件再進(jìn)一步觸發(fā)其他任務(wù),大概的效果是下圖這樣。
?
每個(gè)任務(wù)結(jié)束分別觸發(fā)一個(gè)事件,Task4需要等事件A、B都完成才會(huì)執(zhí)行,并且不會(huì)接著觸發(fā)其他事件。Task5需要等事件B、C都完成,并且會(huì)觸發(fā)事件D,D事件不會(huì)再觸發(fā)任何任務(wù)。當(dāng)然,這些任務(wù)和事件可能在不同的線程上執(zhí)行。
這里再看一下Task任務(wù)的創(chuàng)建代碼,分析一下先決依賴事件與后續(xù)等待事件都是如何產(chǎn)生的。
?
CreateTask的第一個(gè)參數(shù)就是該任務(wù)依賴事件數(shù)組(這里為NULL),如果傳入一個(gè)事件數(shù)組的話,那么當(dāng)前任務(wù)就會(huì)通過SetupPrereqs函數(shù)設(shè)置這些依賴事件,并且在所有依賴事件都觸發(fā)后再將該任務(wù)放到任務(wù)隊(duì)列里面分配給線程執(zhí)行。
當(dāng)執(zhí)行CreateTask時(shí),會(huì)通過FGraphEvent::CreateGraphEvent()構(gòu)建一個(gè)新的后續(xù)事件,再通過函數(shù)ConstructAndDispatchWhenReady返回。這樣我們就可以在當(dāng)前的位置執(zhí)行
?
讓當(dāng)前線程等待該任務(wù)結(jié)束并觸發(fā)事件后再繼續(xù)執(zhí)行,當(dāng)前面這個(gè)事件完成后,就會(huì)調(diào)用DispatchSubsequents()去觸發(fā)他后續(xù)的任務(wù)。WaitUntilTaskCompletes函數(shù)的第二個(gè)參數(shù)必須是當(dāng)前的線程類型而且是帶名字的。
?
4.4 其他相關(guān)技術(shù)細(xì)節(jié)
1.FThreadSafeCounter
通過調(diào)用不同平臺(tái)的原子操作來實(shí)現(xiàn)線程安全的計(jì)數(shù)
?
2. Task的構(gòu)造方式
我們看到相比AsyncTask,TaskGraph的創(chuàng)建可謂是既新奇又復(fù)雜,首先要調(diào)用靜態(tài)的CreateTask,然后又要通過返回值執(zhí)行ConstructAndDispatchWhenReady。那么這么做的目的是什么呢?按照我個(gè)人的理解,主要是為了能把想要的參數(shù)都傳進(jìn)去。其實(shí)每創(chuàng)建一個(gè)任務(wù),都需要傳入兩套參數(shù),一套參數(shù)指定依賴事件,屬于任務(wù)系統(tǒng)的自身特點(diǎn),另一套參數(shù)傳入玩家自定義任務(wù)的相關(guān)參數(shù)。為了實(shí)現(xiàn)這個(gè)效果,UE先通過工廠方法創(chuàng)建抽象任務(wù)把相關(guān)特性保存進(jìn)去,然后通過內(nèi)部的一個(gè)幫助類FConstructor構(gòu)建一個(gè)真正的玩家定義的任務(wù)。如果C++玩的不溜,這樣的方法還真難想出來。(這是我個(gè)人猜測(cè),如果你有更好的理解歡迎留言評(píng)論)
3. FScopedEvent
在上一節(jié)講過,帶有Scope關(guān)鍵字的基本都是同一個(gè)思想,在構(gòu)造的時(shí)候初始化析構(gòu)的時(shí)候執(zhí)行某些特殊的操作。FScopedEvent作用是在當(dāng)前作用域內(nèi)等待觸發(fā),如果沒有激活該事件,就會(huì)一直處于Wait中。
4. WaitUntilTaskCompletes的實(shí)現(xiàn)機(jī)制
顧名思義,該函數(shù)的功能就是在任務(wù)結(jié)束之前保持當(dāng)前線程的等待。不過他的實(shí)現(xiàn)確實(shí)很有趣,第一個(gè)參數(shù)是等待的事件Event,第二個(gè)參數(shù)是當(dāng)前線程類型。如果當(dāng)前的線程沒有任何Task,他會(huì)判斷傳入的事件數(shù)組是否都完成了,完成即可返回,沒有完成就會(huì)構(gòu)建一個(gè)FReturnGraphTask類型的任務(wù),然后執(zhí)行ProcessThreadUntilRequestReturn等所有的依賴事件都完成后才會(huì)返回。
?
如果當(dāng)前的線程有Task任務(wù),他就創(chuàng)建一個(gè)ScopeEvent,并執(zhí)行TriggerEventWhenTasksComplete等待前面?zhèn)魅氲腡asks都完成后再返回。
?
五.總結(jié)
到這里,我們已經(jīng)看到了三種使用多線程的方式,每種機(jī)制里面都有很多技術(shù)點(diǎn)值得我們深入學(xué)習(xí)。關(guān)于機(jī)制的選擇這里再給出一點(diǎn)建議:
對(duì)于消耗大的,復(fù)雜的任務(wù)不建議使用TaskGraph,因?yàn)樗麜?huì)阻塞其他游戲線程的執(zhí)行。即使你不在那幾個(gè)有名字的線程上執(zhí)行,也可能會(huì)影響到游戲的其他邏輯。比如物理計(jì)算相關(guān)的任務(wù)就是在非指定名稱的線程上執(zhí)行的。這種復(fù)雜的任務(wù),建議你自己繼承Runnable創(chuàng)建線程,或者使用AsynTask系統(tǒng)。
而對(duì)于簡單的任務(wù),或者想比較方便的實(shí)現(xiàn)線程的之間的依賴等待關(guān)系,直接扔給TaskGraph就可以了。
另外,不要在非GameThread線程內(nèi)執(zhí)行下面幾個(gè)操作:
?
- 不要 Spawn / Modify/ delete UObjects or AActors
- 不要使用定時(shí)器 TimerManager
- 不要使用任何繪制接口,例如 DrawDebugLine
一開始我也不是很理解,所以就在其他線程里面執(zhí)行了Spawn操作,然后就蹦在了下面的地方。可以看到,SpawnActor的時(shí)候會(huì)執(zhí)行物理數(shù)據(jù)的初始化,而這個(gè)操作是必須要在主線程里面執(zhí)行的,我猜其他的位置肯定還有很多類似的宏。至于原因,我想就是我們最前面提到的“游戲不適合利用多線程優(yōu)化”,游戲GamePlay中各個(gè)部分非常依賴順序,多線程沒辦法很好的處理這些關(guān)系。再者,游戲邏輯如此復(fù)雜,你怎么做到避免“競(jìng)爭條件”呢?到處加鎖么?我想那樣的話,游戲代碼就沒法看了吧。
?
最后,我們?cè)賮硪粡埲腋0蓗
?
總結(jié)
以上是生活随笔為你收集整理的《Exploring in UE4》多线程机制详解[原理分析]的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 萌新资源 | 3D基础——渲染基本原理介
- 下一篇: Unity MMORPG游戏优化经验分享