《Exploring in UE4》多線程機制詳解[原理分析]

轉自:https://zhuanlan.zhihu.com/c_164452593編程

目錄
一.概述
二."標準"多線程
三.AsyncTask系統
3.1 FQueuedThreadPool線程池
3.2 Asyntask與IQueuedWork
3.3 其餘相關技術細節
四.TaskGraph系統
4.1 從Tick函數談起
4.2 TaskGraph系統中的任務與線程
4.3 TaskGraph系統中的任務與事件
4.4 其餘相關技術細節
五.總結

一.概述

多線程是優化項目性能的重要方式之一,遊戲也不例外。雖然常常能看到「遊戲不適合利用多線程優化」的言論,但我我的以爲這句話更多的是針對GamePlay,遊戲中多線程用的一點也很多,好比渲染模塊、物理模塊、網絡通訊、音頻系統、IO等。下圖就展現了UE4引擎運行時的部分線程,可能比你想象的還要多一些。windows

UE4運行時開啓的線程


雖然UE4遵循C++11的標準,可是他並無使用std::thread,而是本身實現了一套多線程機制(應該是從UE3時代就有了,未考證),用法上很像Java。固然,你若是想用std::thread也是徹底沒有問題的。

在UE4裏面,咱們能夠本身繼承FRunnable接口建立單個線程,也能夠直接建立AsyncTask來調用線程池裏面空閒的線程,還能夠經過TaskGraph系統來異步完成一些自定義任務。雖然本質相同,可是用法不一樣,理解上也要花費很多時間,這篇文章會對裏面的各個機制逐個分析並作出總結,但並不會深刻討論線程的實現原理、線程安全等內容。另外,因爲我的接觸多線程編程的時間不長,有一些內容可能不是很準確,歡迎你們一塊兒討論。

二.「標準」多線程

咱們先從最基本的建立方式談起,這裏的「標準」只是一個修飾。其實就是建立一個繼承自FRunnable的類,把這個類要執行的任務分發給其餘線程去執行。FRunnable就是一個很簡單的類,裏面只有5,6個函數接口,爲了與真正的線程區分,我這裏稱FRunnable爲「線程執行體」。數組

//Runnable.h
class CORE_API FRunnable
{
public:
        /**
         * Initializes the runnable object.
         *
         * This method is called in the context of the thread object that aggregates this, not the
         * thread that passes this runnable to a new thread.
         *
         * @return True if initialization was successful, false otherwise
         * @see Run, Stop, Exit
         */
        virtual bool Init()
        {
                return true;
        }

        /**
         * Runs the runnable object.
         *
         * This is where all per object thread work is done. This is only called if the initialization was successful.
         *
         * @return The exit code of the runnable object
         * @see Init, Stop, Exit
         */
        virtual uint32 Run() = 0;

        /**
         * Stops the runnable object.
         *
         * This is called if a thread is requested to terminate early.
         * @see Init, Run, Exit
         */
        virtual void Stop() { }

        /**
         * Exits the runnable object.
         *
         * Called in the context of the aggregating thread to perform any cleanup.
         * @see Init, Run, Stop
         */
        virtual void Exit() { }

        /**
         * Gets single thread interface pointer used for ticking this runnable when multi-threading is disabled.
         * If the interface is not implemented, this runnable will not be ticked when FPlatformProcess::SupportsMultithreading() is false.
         *
        * @return Pointer to the single thread interface or nullptr if not implemented.
         */
        virtual class FSingleThreadRunnable* GetSingleThreadInterface( )
        {
                return nullptr;
        }

        /** Virtual destructor */
        virtual ~FRunnable() { }
};

看起來這麼簡單個類,咱們是否是能夠不繼承他,單獨寫一個類再把這幾個接口放進去呢?固然不行,實際上,在實現多線程的時候,咱們須要將FRunnable做爲參數傳遞到真正的線程裏面,而後才能經過線程去調用FRunnable的Run,也就是咱們具體實現的類的Run方法(經過虛函數覆蓋父類的Run)。所謂真正的線程其實就是FRunnableThread,不一樣平臺的線程都繼承自他,如FRunnableThreadWin,裏面會調用Windows平臺的建立線程的API接口。下圖給出了FRunnable與線程之間的關係類圖:安全

在實現的時候,你須要繼承FRunnable並重寫他的那幾個函數,Run()裏面表示你在線程裏面想要執行的邏輯。具體的實現方式網上有不少案例,這裏給出UE4Wiki的教程連接:

Multi-Threading: How to Create Threads in UE4

三.AsyncTask系統

說完了UE4「標準」線程的使用,下面咱們來談談稍微複雜一點的AsyncTask系統。AsyncTask系統是一套基於線程池的異步任務處理系統。若是你沒有接觸過UE4多線程,用搜索引擎搜索UE4多線程時可能就會看到相似下面這樣的用法。服務器

//AsyncWork.h
        class ExampleAsyncTask : public FNonAbandonableTask
        {
                friend class FAsyncTask<ExampleAsyncTask>;

                int32 ExampleData;

                ExampleAsyncTask(int32 InExampleData)
                 : ExampleData(InExampleData)
                {
                }

                void DoWork()
                {
                        ... do the work here
                }

                FORCEINLINE TStatId GetStatId() const
                {
                        RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
                }
        };

        void Example()
        {

                //start an example job

                FAsyncTask<ExampleAsyncTask>* MyTask = new FAsyncTask<ExampleAsyncTask>( 5 );
                MyTask->StartBackgroundTask();

                //--or --

                MyTask->StartSynchronousTask();

                //to just do it now on this thread
                //Check if the task is done :

                if (MyTask->IsDone())
                {
                }

                //Spinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame.
                //Ensure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases.

                MyTask->EnsureCompletion();
                delete Task;
        }

 沒錯,這就是官方代碼裏面給出的一種異步處理的解決方案示例。不過你可能更在乎的是這個所謂多線程的用法,看起來很是簡單,可是卻找不到任何帶有「Thread」或「Runnable」的字樣,那麼他也是用Runnable的方式作的麼?答案確定是Yes。只不過封裝的比較深,須要咱們深刻源碼才能明白其中的原理。網絡

注:Andriod多線程開發裏面也會用到AsyncTask,兩者的實現原理很是類似。多線程

3.1 FQueuedThreadPool線程池

在介紹AsynTask以前先講一下UE裏面的線程池,FQueuedThreadPool。和通常的線程池實現相似,線程池裏面維護了多個線程FQueuedThread與多個任務隊列IQueuedWork,線程是按照隊列的方式來排列的。在引擎PreInit的時候執行相關的初始化操做,代碼以下框架

// FEngineLoop.PreInit   LaunchEngineLoop.cpp
if (FPlatformProcess::SupportsMultithreading())
{
        {
                GThreadPool = FQueuedThreadPool::Allocate();
                int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfWorkerThreadsToSpawn();

                // we are only going to give dedicated servers one pool thread
                if (FPlatformProperties::IsServerOnly())
                {
                    NumThreadsInThreadPool = 1;
                }
                verify(GThreadPool->Create(NumThreadsInThreadPool, 128 * 1024));
        }
#ifUSE_NEW_ASYNC_IO
        {
                GIOThreadPool = FQueuedThreadPool::Allocate();
                int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfIOWorkerThreadsToSpawn();
                if (FPlatformProperties::IsServerOnly())
                {
                    NumThreadsInThreadPool = 2;
                }
                verify(GIOThreadPool->Create(NumThreadsInThreadPool, 16 * 1024, TPri_AboveNormal));
        }
#endif// USE_NEW_ASYNC_IO

#ifWITH_EDITOR
        // when we are in the editor we like to do things like build lighting and such
        // this thread pool can be used for those purposes
        GLargeThreadPool = FQueuedThreadPool::Allocate();
        int32 NumThreadsInLargeThreadPool = FMath::Max(FPlatformMisc::NumberOfCoresIncludingHyperthreads() - 2, 2);
                
        verify(GLargeThreadPool->Create(NumThreadsInLargeThreadPool, 128 * 1024));
#endif
}

這段代碼咱們能夠看出,專有服務器的線程池GThreadPool默認只開一個線程,非專有服務器的根據核數開(CoreNum-1)個線程。編輯器模式會另外再建立一個線程池GLargeThreadPool,包含(LogicalCoreNum-2)個線程,用來處理貼圖的壓縮和編碼相關內容。

在線程池裏面全部的線程都是FQueuedThread類型,不過更確切的說FQueuedThread是繼承自FRunnable的線程執行體,每一個FQueuedThread裏面包含一個FRunnableThread做爲內部成員。

相比通常的線程,FQueuedThread裏面多了一個成員FEvent* DoWorkEvent,也就是說FQueuedThread裏面是有一個事件觸發機制的。那麼這個事件機制的做用是什麼?通常狀況下來講,就是在沒有任務的時候掛起這個線程,在添加並分配給該線程任務的時候激活他,不過你能夠靈活運用它,在你須要的時候去動態控制線程任務的執行與暫停。前面咱們在給線程池初始化的時候,經過FQueuedThreadPool的Create函數建立了多個FQueuedThread,而後每一個FQueuedThread會執行Run函數,裏面有一段邏輯以下:異步

//ThreadingBase.cpp
bool bContinueWaiting = true;
while(bContinueWaiting )
{                                
        DECLARE_SCOPE_CYCLE_COUNTER(TEXT( "FQueuedThread::Run.WaitForWork" ), STAT_FQueuedThread_Run_WaitForWork, STATGROUP_ThreadPoolAsyncTasks );
        // Wait for some work to do
        bContinueWaiting = !DoWorkEvent->Wait( 10 );
}
//windows平臺下的wait
bool FEventWin::Wait(uint32 WaitTime, const bool bIgnoreThreadIdleStats/*= false*/)
{
        WaitForStats();

        SCOPE_CYCLE_COUNTER(STAT_EventWait );
        check(Event );

        FThreadIdleStats::FScopeIdleScope(bIgnoreThreadIdleStats );
        return (WaitForSingleObject( Event, WaitTime ) == WAIT_OBJECT_0);
}

咱們看到,當DoWorkEvent執行Wait的時候,若是該線程的Event處於無信號狀態(默認剛建立是無信號的),那麼wait會等待10毫秒並返回false,線程處於While無限循環中。若是線程池添加了任務(AddQueuedWork)並執行了DoWorkEvent的Trigger函數,那麼Event就會被設置爲有信號,Wait函數就會返回true,隨後線程跳出循環進而處理任務。

編輯器

注:FQueuedThread裏的DoWorkEvent是經過FPlatformProcess::GetSynchEventFromPool();從EventPool裏面獲取的。WaitForSingleObject等內容涉及到Windows下的事件機制,你們能夠自行到網上搜索相關的使用,這裏給出一個官方的使用案例。


目前咱們接觸的類之間的關係以下圖:

3.2 Asyntask與IQueuedWork

線程池的任務IQueuedWork自己是一個接口,因此得有具體實現。這裏你就應該能猜到,所謂的AsynTask其實就是對IQueuedWork的具體實現。這裏AsynTask泛指FAsyncTask與FAutoDeleteAsyncTask兩個類,咱們先從FAsyncTask提及。

FAsyncTask有幾個特色,

  • FAsyncTask是一個模板類,真正的AsyncTask須要你本身寫。經過DoWork提供你要執行的具體任務,而後把你的類做爲模板參數傳過去
  • 使用FAsyncTask就默認你要使用UE提供的線程池FQueuedThreadPool,前面代碼裏說明了在引擎PreInit的時候會初始化線程池並返回一個指針GThreadPool。在執行FAsyncTask任務時,若是你在執行StartBackgroundTask的時候會默認使用GThreadPool線程池,固然你也能夠在參數裏面指定本身建立的線程池
  • 建立FAsyncTask並不必定要使用新的線程,你能夠調用函數StartSynchronousTask直接在當前線程上執行任務
  • FAsyncTask自己包含一個DoneEvent,任務執行完成的時候會激活該事件。當你想等待一個任務完成時再作其餘操做,就能夠調用EnsureCompletion函數,他能夠從隊列裏面取出來還沒被執行的任務放到當前線程來作,也能夠掛起當前線程等待DoneEvent激活後再往下執行



FAutoDeleteAsyncTask與FAsyncTask是類似的,可是有一些差別,

  • 默認使用UE提供的線程池FQueuedThreadPool,沒法使用其餘線程池
  • FAutoDeleteAsyncTask在任務完成後會經過線程池的Destroy函數刪除自身或者在執行DoWork後刪除自身,而FAsyncTask須要手動delete
  • 包含FAsyncTask的特色1和特色3



總的來講,AsyncTask系統實現的多線程與你本身字節繼承FRunnable實現的原理類似,不過他在用法上比較簡單,並且還能夠直接借用UE4提供的線程池,很方便。

最後咱們再來梳理一下這些類之間的關係:

AsyncTask系統相關類圖


3.3 其餘相關技術細節

你們在看源碼的時候可能會遇到一些疑問,這裏簡單列舉並解釋一下

1. FScopeLock

FScopeLock是UE提供的一種基於做用域的鎖,思想相似RAII機制。在構造時對當前區域加鎖,離開做用域時執行析構並解鎖。UE裏面有不少帶有「Scope」關鍵字的類,如移動組件中的FScopedMovementUpdate,Task系統中的FScopeCycleCounter,FScopedEvent等,他們的實現思路是相似的。

2. FNonAbandonableTask

繼承FNonAbandonableTask的Task不能夠在執行階段終止,即便執行Abandon函數也會去觸發DoWork函數。

      // FAutoDeleteAsyncTask
        virtual void Abandon(void)
        {
                if (Task.CanAbandon())
                {
                        Task.Abandon();
                        delete this;
                }
                else
                {
                        DoWork();
                }
        }
        // FAsyncTask
        virtual void Abandon(void)
        {
                if (Task.CanAbandon())
                {
                        Task.Abandon();
                        check(WorkNotFinishedCounter.GetValue() == 1);
                        WorkNotFinishedCounter.Decrement();
                }
                else
                {
                        DoWork();
                }
                FinishThreadedWork();
        }

3.AsyncTask與轉發構造

經過本章節開始的例子,咱們知道建立自定義任務的方式以下

FAsyncTask<ExampleAsyncTask>*MyTask= new FAsyncTask<ExampleAsyncTask>(5);

括號裏面的5會以參數轉發的方式傳到的ExampleAsyncTask構造函數裏面,這一步涉及到C++11的右值引用與轉發構造,具體細節能夠去網上搜索一下。

/** Forwarding constructor. */
template <typename Arg0Type, typename... ArgTypes>
FAsyncTask(Arg0Type&& Arg0, ArgTypes&&... Args)
        : Task(Forward<Arg0Type>(Arg0), Forward<ArgTypes>(Args)...)
{
        Init();
}

四.TaskGraph系統

說完了FAsyncTask系統,接下來咱們再談談更復雜的TaskGraph系統(應該不會有比他更復雜的了)。Task Graph 系統是UE4一套抽象的異步任務處理系統,能夠建立多個多線程任務,指定各個任務之間的依賴關係,按照該關係來依次處理任務。具體的實現方式網上也有不少案例,這裏先給出UE4Wiki的教程連接:

Multi-Threading: Task Graph System

建議你們先了解其用法,而後再往下閱讀。

4.1 從Tick函數談起

平時調試的時候,咱們隨便找個Tick斷點一下都能看到相似下圖這樣的函數堆棧。若是你前面的章節都看懂的話,這個堆棧也能大概理解。World在執行Tick的時候,觸發了FNamedTaskThread線程去執行任務(FTickFunctionTask),任務FTickFunctionTask具體的工做內容就是執行ACtorComponent的Tick函數。其實,這個堆棧也說明了全部Actor與Component的Tick都是經過TaskGraph系統來執行的。

組件Tick的函數堆棧


不過你可能仍是會有不少問題,TaskGraph斷點爲何是在主線程裏面?FNamedTaskThread是什麼意思?FTickFunctionTask究竟是在哪一個線程執行?答案在下一小節逐步給出。

4.2 TaskGraph系統中的任務與線程

既然是Task系統,那麼應該能猜到他和前面的AsyncTask系統類似,咱們能夠建立多個Task任務而後分配給不一樣的線程去執行。在TaskGraph系統裏面,任務類也是咱們本身建立的,如FTickFunctionTask、FReturnGraphTask等,裏面須要聲明DoTask函數來表示要執行的任務內容,GetDesiredThread函數來表示要在哪一個線程上面執行,大概的樣子以下:

class FMyTestTask
{
        public:
         FMyTestTask()//send in property defaults here
        {
        }
        static const TCHAR*GetTaskName()
        {
                return TEXT("FMyTestTask");
        }
        FORCEINLINE static TStatId GetStatId()
        {
                RETURN_QUICK_DECLARE_CYCLE_STAT(FMyTestTask, STATGROUP_TaskGraphTasks);
        }
        /** return the thread for this task **/
        static ENamedThreads::Type GetDesiredThread()
        {
                return ENamedThreads::AnyThread;
        }

        /*
        namespace ESubsequentsMode
       {
                enum Type
                {
                        // 存在後續任務
                        TrackSubsequents,
                        // 沒有後續任務
                        FireAndForget
                };
        }
        */
        static ESubsequentsMode::Type GetSubsequentsMode()
        {
                return ESubsequentsMode::TrackSubsequents;
        }

        void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
        {
                
        }
};

而線程在該系統裏面稱爲FWorkerThread,經過全局的單例類FTaskGraphImplementation來控制建立和分配任務的,默認狀況下會開啓5個基本線程,額外線程的數量則由下面的函數NumberOfWorkerThreadsToSpawn來決定,FTaskGraphImplementation的初始化在FEngineLoop.PreInit裏面進行。固然若是平臺自己不支持多線程,那麼其餘的工做也會在GameThread裏面進行。

FTaskGraphImplementation(int32)
{
        bCreatedHiPriorityThreads = !!ENamedThreads::bHasHighPriorityThreads;
        bCreatedBackgroundPriorityThreads = !!ENamedThreads::bHasBackgroundThreads;

        int32 MaxTaskThreads = MAX_THREADS;
        int32 NumTaskThreads = FPlatformMisc::NumberOfWorkerThreadsToSpawn();

        // if we don't want any performance-based threads, then force the task graph to not create any worker threads, and run in game thread
        if (!FPlatformProcess::SupportsMultithreading())
        {
                // this is the logic that used to be spread over a couple of places, that will make the rest of this function disable a worker thread
                // @todo: it could probably be made simpler/clearer
                // this - 1 tells the below code there is no rendering thread
                MaxTaskThreads = 1;
                NumTaskThreads = 1;
                LastExternalThread = (ENamedThreads::Type)(ENamedThreads::ActualRenderingThread - 1);
                bCreatedHiPriorityThreads = false;
                bCreatedBackgroundPriorityThreads = false;
                ENamedThreads::bHasBackgroundThreads = 0;
                ENamedThreads::bHasHighPriorityThreads = 0;
        }
        else
        {
                LastExternalThread = ENamedThreads::ActualRenderingThread;
        }
                
        NumNamedThreads = LastExternalThread + 1;

        NumTaskThreadSets = 1 + bCreatedHiPriorityThreads + bCreatedBackgroundPriorityThreads;

        // if we don't have enough threads to allow all of the sets asked for, then we can't create what was asked for.
        check(NumTaskThreadSets == 1 || FMath::Min<int32>(NumTaskThreads * NumTaskThreadSets + NumNamedThreads, MAX_THREADS) == NumTaskThreads * NumTaskThreadSets + NumNamedThreads);
        NumThreads = FMath::Max<int32>(FMath::Min<int32>(NumTaskThreads * NumTaskThreadSets + NumNamedThreads, MAX_THREADS), NumNamedThreads + 1);
        .......
}
//GenericPlatformMisc.cpp
int32 FGenericPlatformMisc::NumberOfWorkerThreadsToSpawn()
{
        static int32 MaxGameThreads = 4;
        static int32 MaxThreads = 16;

        int32 NumberOfCores = FPlatformMisc::NumberOfCores();//物理核數,4核8線程的機器返回的是4
        int32 MaxWorkerThreadsWanted = (IsRunningGame() || IsRunningDedicatedServer() || IsRunningClientOnly()) ? MaxGameThreads :MaxThreads;
        // need to spawn at least one worker thread (see FTaskGraphImplementation)
        return FMath::Max(FMath::Min(NumberOfCores - 1, MaxWorkerThreadsWanted), 1);
}

前面提到的FWorkerThread雖然能夠理解爲工做線程,但其實他不是真正的線程。FWorkerThread裏面有兩個重要成員,一個是FRunnableThread* RunnableThread,也就是真正的線程。另外一個是FTaskThreadBase* TaskGraphWorker,即繼承自FRunnable的線程執行體。FTaskThreadBase有兩個子類,FTaskThreadAnyThread和FNamedTaskThread,分別表示非指定名稱的任意Task線程執行體和有名字的Task線程執行體。咱們平時說的渲染線程、遊戲線程就是有名稱的Task線程,而那些咱們建立後尚未使用到的線程就是非指定名稱的任意線程。

非指定名稱的任意線程


在引擎初始化FTaskGraphImplementation的時候,咱們就會默認構建24個FWorkerThread工做線程(這裏支持最大的線程數量也就是24),其中裏面有5個是默認帶名字的線程,StatThread、RHIThread、AudioThread、GameThread、ActualRenderingThread,還有前面提到的N個非指定名稱的任意線程,這個N由CPU核數決定。對於帶有名字的線程,他不須要建立新的Runnable線程,由於他們會在其餘的時機建立,如StatThread以及RenderingThread會在FEngineLoop.PreInit裏建立。而那N個非指定名稱的任意線程,則須要在一開始就手動建立Runnable線程,同時設置其優先級比前面線程的優先級要低。到這裏,咱們應該能夠理解,有名字的線程專門要作他名字對應的事情,非指定名稱的任意線程則能夠用來處理其餘的工做,咱們在CreateTask建立任務時會經過本身寫好的函數決定當前任務應該在哪一個線程執行。

運行中全部的WorldThreads


如今咱們能夠先回答一下上一節的問題了,FTickFunctionTask究竟是在哪一個線程執行?答案是遊戲主線程,咱們能夠看到FTickFunctionTask的Desired線程是Context.Thread,而Context.Thread是在下圖賦值的,具體細節參考FTickTaskManager與FTickTaskLevel的使用。

/** return the thread for this task **/
FORCEINLINEENamedThreads::TypeGetDesiredThread()
{
        return Context.Thread;
}

context線程類型的初始化


這裏咱們再思考一下,若是咱們將多個任務投放到一個線程那麼他們是按照什麼順序執行的呢?這個答案須要分兩種狀況解答,對於投放到FTaskThreadAnyThread執行的任務會在建立的時候按照優先級放到IncomingAnyThreadTasks數組裏面,而後每次線程完成任務後會從這個數組裏面彈出未執行的任務來執行,他的特色是咱們有權利隨時修改和調整這個任務隊列。而對於投放到FNamedTaskThread執行的任務,會被放到其自己維護的隊列裏面,經過FThreadTaskQueue來處理執行順序,一旦放到這個隊列裏面,咱們就沒法隨意調整任務了。

4.3 TaskGraph系統中的任務與事件

雖然前面已經比較細緻的描述了TaskGraph系統的框架,可是一個很是重要的特性咱們還沒講到,就是任務依賴的實現原理。怎麼理解任務依賴呢?簡單來講,就是一個任務的執行可能依賴於多個事件對象,這些事件對象都觸發以後纔會執行這個任務。而這個任務完成後,又可能觸發其餘事件,其餘事件再進一步觸發其餘任務,大概的效果是下圖這樣。

任務與事件的依賴關係圖


每一個任務結束分別觸發一個事件,Task4須要等事件A、B都完成纔會執行,而且不會接着觸發其餘事件。Task5須要等事件B、C都完成,而且會觸發事件D,D事件不會再觸發任何任務。固然,這些任務和事件可能在不一樣的線程上執行。

這裏再看一下Task任務的建立代碼,分析一下先決依賴事件與後續等待事件都是如何產生的。

FGraphEventRef Join=TGraphTask<FVictoryTestTask>::CreateTask(NULL, ENamedThreads::GameThread).ConstructAndDispatchWhenReady();

CreateTask的第一個參數就是該任務依賴事件數組(這裏爲NULL),若是傳入一個事件數組的話,那麼當前任務就會經過SetupPrereqs函數設置這些依賴事件,而且在全部依賴事件都觸發後再將該任務放到任務隊列裏面分配給線程執行。

當執行CreateTask時,會經過FGraphEvent::CreateGraphEvent()構建一個新的後續事件,再經過函數ConstructAndDispatchWhenReady返回。這樣咱們就能夠在當前的位置執行

FTaskGraphInterface::Get().WaitUntilTaskCompletes(Join, ENamedThreads::GameThread_Local);

讓當前線程等待該任務結束並觸發事件後再繼續執行,當前面這個事件完成後,就會調用DispatchSubsequents()去觸發他後續的任務。WaitUntilTaskCompletes函數的第二個參數必須是當前的線程類型並且是帶名字的。

Task系統相關類圖


4.4 其餘相關技術細節

1.FThreadSafeCounter

經過調用不一樣平臺的原子操做來實現線程安全的計數

int32 Add( int32 Amount )
{
        return FPlatformAtomics::InterlockedAdd(&Counter, Amount);
}

2. Task的構造方式

咱們看到相比AsyncTask,TaskGraph的建立可謂是既新奇又複雜,首先要調用靜態的CreateTask,而後又要經過返回值執行ConstructAndDispatchWhenReady。那麼這麼作的目的是什麼呢?按照我我的的理解,主要是爲了能把想要的參數都傳進去。其實每建立一個任務,都須要傳入兩套參數,一套參數指定依賴事件,屬於任務系統的自身特色,另外一套參數傳入玩家自定義任務的相關參數。爲了實現這個效果,UE先經過工廠方法建立抽象任務把相關特性保存進去,而後經過內部的一個幫助類FConstructor構建一個真正的玩家定義的任務。若是C++玩的不溜,這樣的方法還真難想出來。(這是我我的猜想,若是你有更好的理解歡迎留言評論)

3. FScopedEvent

在上一節講過,帶有Scope關鍵字的基本都是同一個思想,在構造的時候初始化析構的時候執行某些特殊的操做。FScopedEvent做用是在當前做用域內等待觸發,若是沒有激活該事件,就會一直處於Wait中。

4. WaitUntilTaskCompletes的實現機制

顧名思義,該函數的功能就是在任務結束以前保持當前線程的等待。不過他的實現確實頗有趣,第一個參數是等待的事件Event,第二個參數是當前線程類型。若是當前的線程沒有任何Task,他會判斷傳入的事件數組是否都完成了,完成便可返回,沒有完成就會構建一個FReturnGraphTask類型的任務,而後執行ProcessThreadUntilRequestReturn等全部的依賴事件都完成後纔會返回。

// named thread process tasks while we wait
TGraphTask<FReturnGraphTask>::CreateTask(&Tasks, CurrentThread).ConstructAndDispatchWhenReady(CurrentThread);
ProcessThreadUntilRequestReturn(CurrentThread);

若是當前的線程有Task任務,他就建立一個ScopeEvent,並執行TriggerEventWhenTasksComplete等待前面傳入的Tasks都完成後再返回。

FScopedEvent Event;
TriggerEventWhenTasksComplete(Event.Get(), Tasks, CurrentThreadIfKnown);

五.總結

到這裏,咱們已經看到了三種使用多線程的方式,每種機制裏面都有不少技術點值得咱們深刻學習。關於機制的選擇這裏再給出一點建議:

對於消耗大的,複雜的任務不建議使用TaskGraph,由於他會阻塞其餘遊戲線程的執行。即便你不在那幾個有名字的線程上執行,也可能會影響到遊戲的其餘邏輯。好比物理計算相關的任務就是在非指定名稱的線程上執行的。這種複雜的任務,建議你本身繼承Runnable建立線程,或者使用AsynTask系統。

而對於簡單的任務,或者想比較方便的實現線程的之間的依賴等待關係,直接扔給TaskGraph就能夠了。

另外,不要在非GameThread線程內執行下面幾個操做:

  • 不要 Spawn / Modify/ delete UObjects or AActors
  • 不要使用定時器 TimerManager
  • 不要使用任何繪製接口,例如 DrawDebugLine



一開始我也不是很理解,因此就在其餘線程裏面執行了Spawn操做,而後就蹦在了下面的地方。能夠看到,SpawnActor的時候會執行物理數據的初始化,而這個操做是必需要在主線程裏面執行的,我猜其餘的位置確定還有不少相似的宏。至於緣由,我想就是咱們最前面提到的「遊戲不適合利用多線程優化」,遊戲GamePlay中各個部分很是依賴順序,多線程沒辦法很好的處理這些關係。再者,遊戲邏輯如此複雜,你怎麼作到避免「競爭條件」呢?處處加鎖麼?我想那樣的話,遊戲代碼就無法看了吧。

在其餘線程Spawn致使崩潰


最後,咱們再來一張全家福吧~

多線程系統類圖(完整)

相關文章
相關標籤/搜索