日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

《Exploring in UE4》多线程机制详解[原理分析]

發(fā)布時間:2024/8/26 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《Exploring in UE4》多线程机制详解[原理分析] 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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

一.概述

多線程是優(yōu)化項目性能的重要方式之一,游戲也不例外。雖然經常能看到“游戲不適合利用多線程優(yōu)化”的言論,但我個人覺得這句話更多的是針對GamePlay,游戲中多線程用的一點也不少,比如渲染模塊、物理模塊、網絡通信、音頻系統(tǒng)、IO等。下圖就展示了UE4引擎運行時的部分線程,可能比你想象的還要多一些。
?

UE4運行時開啟的線程


雖然UE4遵循C++11的標準,但是他并沒有使用std::thread,而是自己實現了一套多線程機制(應該是從UE3時代就有了,未考證),用法上很像Java。當然,你如果想用std::thread也是完全沒有問題的。

在UE4里面,我們可以自己繼承FRunnable接口創(chuàng)建單個線程,也可以直接創(chuàng)建AsyncTask來調用線程池里面空閑的線程,還可以通過TaskGraph系統(tǒng)來異步完成一些自定義任務。雖然本質相同,但是用法不同,理解上也要花費不少時間,這篇文章會對里面的各個機制逐個分析并做出總結,但并不會深入討論線程的實現原理、線程安全等內容。另外,由于個人接觸多線程編程的時間不長,有一些內容可能不是很準確,歡迎大家一起討論。

二.“標準”多線程

我們先從最基本的創(chuàng)建方式談起,這里的“標準”只是一個修飾。其實就是創(chuàng)建一個繼承自FRunnable的類,把這個類要執(zhí)行的任務分發(fā)給其他線程去執(zhí)行。FRunnable就是一個很簡單的類,里面只有5,6個函數接口,為了與真正的線程區(qū)分,我這里稱FRunnable為“線程執(zhí)行體”。
?

  • //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平臺的創(chuàng)建線程的API接口。下圖給出了FRunnable與線程之間的關系類圖:
    ?


    在實現的時候,你需要繼承FRunnable并重寫他的那幾個函數,Run()里面表示你在線程里面想要執(zhí)行的邏輯。具體的實現方式網上有很多案例,這里給出UE4Wiki的教程鏈接:

    Multi-Threading: How to Create Threads in UE4

    三.AsyncTask系統(tǒng)

    說完了UE4“標準”線程的使用,下面我們來談談稍微復雜一點的AsyncTask系統(tǒng)。AsyncTask系統(tǒng)是一套基于線程池的異步任務處理系統(tǒng)。如果你沒有接觸過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多線程開發(fā)里面也會用到AsyncTask,二者的實現原理非常相似。


    3.1 FQueuedThreadPool線程池

    在介紹AsynTask之前先講一下UE里面的線程池,FQueuedThreadPool。和一般的線程池實現類似,線程池里面維護了多個線程FQueuedThread與多個任務隊列IQueuedWork,線程是按照隊列的方式來排列的。在引擎PreInit的時候執(zhí)行相關的初始化操作,代碼如下
    ?

  • // 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)個線程。編輯器模式會另外再創(chuàng)建一個線程池GLargeThreadPool,包含(LogicalCoreNum-2)個線程,用來處理貼圖的壓縮和編碼相關內容。

    在線程池里面所有的線程都是FQueuedThread類型,不過更確切的說FQueuedThread是繼承自FRunnable的線程執(zhí)行體,每個FQueuedThread里面包含一個FRunnableThread作為內部成員。

    相比一般的線程,FQueuedThread里面多了一個成員FEvent* DoWorkEvent,也就是說FQueuedThread里面是有一個事件觸發(fā)機制的。那么這個事件機制的作用是什么?二手手游拍賣平臺一般情況下來說,就是在沒有任務的時候掛起這個線程,在添加并分配給該線程任務的時候激活他,不過你可以靈活運用它,在你需要的時候去動態(tài)控制線程任務的執(zhí)行與暫停。前面我們在給線程池初始化的時候,通過FQueuedThreadPool的Create函數創(chuàng)建了多個FQueuedThread,然后每個FQueuedThread會執(zhí)行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執(zhí)行Wait的時候,如果該線程的Event處于無信號狀態(tài)(默認剛創(chuàng)建是無信號的),那么wait會等待10毫秒并返回false,線程處于While無限循環(huán)中。如果線程池添加了任務(AddQueuedWork)并執(zhí)行了DoWorkEvent的Trigger函數,那么Event就會被設置為有信號,Wait函數就會返回true,隨后線程跳出循環(huán)進而處理任務。
    ?

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


    目前我們接觸的類之間的關系如下圖:
    ?


    3.2 Asyntask與IQueuedWork

    線程池的任務IQueuedWork本身是一個接口,所以得有具體實現。這里你就應該能猜到,所謂的AsynTask其實就是對IQueuedWork的具體實現。這里AsynTask泛指FAsyncTask與FAutoDeleteAsyncTask兩個類,我們先從FAsyncTask說起。

    FAsyncTask有幾個特點,
    ?

    • FAsyncTask是一個模板類,真正的AsyncTask需要你自己寫。通過DoWork提供你要執(zhí)行的具體任務,然后把你的類作為模板參數傳過去
    • 使用FAsyncTask就默認你要使用UE提供的線程池FQueuedThreadPool,前面代碼里說明了在引擎PreInit的時候會初始化線程池并返回一個指針GThreadPool。在執(zhí)行FAsyncTask任務時,如果你在執(zhí)行StartBackgroundTask的時候會默認使用GThreadPool線程池,當然你也可以在參數里面指定自己創(chuàng)建的線程池
    • 創(chuàng)建FAsyncTask并不一定要使用新的線程,你可以調用函數StartSynchronousTask直接在當前線程上執(zhí)行任務
    • FAsyncTask本身包含一個DoneEvent,任務執(zhí)行完成的時候會激活該事件。當你想等待一個任務完成時再做其他操作,就可以調用EnsureCompletion函數,他可以從隊列里面取出來還沒被執(zhí)行的任務放到當前線程來做,也可以掛起當前線程等待DoneEvent激活后再往下執(zhí)行



    FAutoDeleteAsyncTask與FAsyncTask是相似的,但是有一些差異,
    ?

    • 默認使用UE提供的線程池FQueuedThreadPool,無法使用其他線程池
    • FAutoDeleteAsyncTask在任務完成后會通過線程池的Destroy函數刪除自身或者在執(zhí)行DoWork后刪除自身,而FAsyncTask需要手動delete
    • 包含FAsyncTask的特點1和特點3



    總的來說,AsyncTask系統(tǒng)實現的多線程與你自己字節(jié)繼承FRunnable實現的原理相似,不過他在用法上比較簡單,而且還可以直接借用UE4提供的線程池,很方便。

    最后我們再來梳理一下這些類之間的關系:
    ?

    AsyncTask系統(tǒng)相關類圖


    3.3 其他相關技術細節(jié)

    大家在看源碼的時候可能會遇到一些疑問,這里簡單列舉并解釋一下

    1. FScopeLock

    FScopeLock是UE提供的一種基于作用域的鎖,思想類似RAII機制。在構造時對當前區(qū)域加鎖,離開作用域時執(zhí)行析構并解鎖。UE里面有很多帶有“Scope”關鍵字的類,如移動組件中的FScopedMovementUpdate,Task系統(tǒng)中的FScopeCycleCounter,FScopedEvent等,他們的實現思路是類似的。

    2. FNonAbandonableTask

    繼承FNonAbandonableTask的Task不可以在執(zhí)行階段終止,即使執(zhí)行Abandon函數也會去觸發(fā)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與轉發(fā)構造

    通過本章節(jié)開始的例子,我們知道創(chuàng)建自定義任務的方式如下

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

    括號里面的5會以參數轉發(fā)的方式傳到的ExampleAsyncTask構造函數里面,這一步涉及到C++11的右值引用與轉發(fā)構造,具體細節(jié)可以去網上搜索一下。
    ?

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


    四.TaskGraph系統(tǒng)

    說完了FAsyncTask系統(tǒng),接下來我們再談談更復雜的TaskGraph系統(tǒng)(應該不會有比他更復雜的了)。Task Graph 系統(tǒng)是UE4一套抽象的異步任務處理系統(tǒng),可以創(chuàng)建多個多線程任務,指定各個任務之間的依賴關系,按照該關系來依次處理任務。具體的實現方式網上也有很多案例,這里先給出UE4Wiki的教程鏈接:

    Multi-Threading: Task Graph System

    建議大家先了解其用法,然后再往下閱讀。

    4.1 從Tick函數談起

    平時調試的時候,我們隨便找個Tick斷點一下都能看到類似下圖這樣的函數堆棧。如果你前面的章節(jié)都看懂的話,這個堆棧也能大概理解。World在執(zhí)行Tick的時候,觸發(fā)了FNamedTaskThread線程去執(zhí)行任務(FTickFunctionTask),任務FTickFunctionTask具體的工作內容就是執(zhí)行ACtorComponent的Tick函數。其實,這個堆棧也說明了所有Actor與Component的Tick都是通過TaskGraph系統(tǒng)來執(zhí)行的。
    ?

    組件Tick的函數堆棧


    不過你可能還是會有很多問題,TaskGraph斷點為什么是在主線程里面?FNamedTaskThread是什么意思?FTickFunctionTask到底是在哪個線程執(zhí)行?答案在下一小節(jié)逐步給出。

    4.2 TaskGraph系統(tǒng)中的任務與線程

    既然是Task系統(tǒng),那么應該能猜到他和前面的AsyncTask系統(tǒng)相似,我們可以創(chuàng)建多個Task任務然后分配給不同的線程去執(zhí)行。在TaskGraph系統(tǒng)里面,任務類也是我們自己創(chuàng)建的,如FTickFunctionTask、FReturnGraphTask等,里面需要聲明DoTask函數來表示要執(zhí)行的任務內容,GetDesiredThread函數來表示要在哪個線程上面執(zhí)行,大概的樣子如下:
    ?

  • 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
  • ? ? ? ? ? ? ? ? {
  • ? ? ? ? ? ? ? ? ? ? ? ? // 存在后續(xù)任務
  • ? ? ? ? ? ? ? ? ? ? ? ? TrackSubsequents,
  • ? ? ? ? ? ? ? ? ? ? ? ? // 沒有后續(xù)任務
  • ? ? ? ? ? ? ? ? ? ? ? ? FireAndForget
  • ? ? ? ? ? ? ? ? };
  • ? ? ? ? }
  • ? ? ? ? */
  • ? ? ? ? static ESubsequentsMode::Type GetSubsequentsMode()
  • ? ? ? ? {
  • ? ? ? ? ? ? ? ? return ESubsequentsMode::TrackSubsequents;
  • ? ? ? ? }
  • ? ? ? ? void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
  • ? ? ? ? {
  • ? ? ? ? ? ? ? ?
  • ? ? ? ? }
  • };
  • 復制代碼


    而線程在該系統(tǒng)里面稱為FWorkerThread,通過全局的單例類FTaskGraphImplementation來控制創(chuàng)建和分配任務的,默認情況下會開啟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的線程執(zhí)行體。FTaskThreadBase有兩個子類,FTaskThreadAnyThread和FNamedTaskThread,分別表示非指定名稱的任意Task線程執(zhí)行體和有名字的Task線程執(zhí)行體。我們平時說的渲染線程、游戲線程就是有名稱的Task線程,而那些我們創(chuàng)建后還沒有使用到的線程就是非指定名稱的任意線程。
    ?

    非指定名稱的任意線程


    在引擎初始化FTaskGraphImplementation的時候,我們就會默認構建24個FWorkerThread工作線程(這里支持最大的線程數量也就是24),其中里面有5個是默認帶名字的線程,StatThread、RHIThread、AudioThread、GameThread、ActualRenderingThread,還有前面提到的N個非指定名稱的任意線程,這個N由CPU核數決定。對于帶有名字的線程,他不需要創(chuàng)建新的Runnable線程,因為他們會在其他的時機創(chuàng)建,如StatThread以及RenderingThread會在FEngineLoop.PreInit里創(chuàng)建。而那N個非指定名稱的任意線程,則需要在一開始就手動創(chuàng)建Runnable線程,同時設置其優(yōu)先級比前面線程的優(yōu)先級要低。到這里,我們應該可以理解,有名字的線程專門要做他名字對應的事情,非指定名稱的任意線程則可以用來處理其他的工作,我們在CreateTask創(chuàng)建任務時會通過自己寫好的函數決定當前任務應該在哪個線程執(zhí)行。
    ?

    運行中所有的WorldThreads


    現在我們可以先回答一下上一節(jié)的問題了,FTickFunctionTask到底是在哪個線程執(zhí)行?答案是游戲主線程,我們可以看到FTickFunctionTask的Desired線程是Context.Thread,而Context.Thread是在下圖賦值的,具體細節(jié)參考FTickTaskManager與FTickTaskLevel的使用。
    ?

  • /** return the thread for this task **/
  • FORCEINLINEENamedThreads::TypeGetDesiredThread()
  • {
  • ? ? ? ? return Context.Thread;
  • }
  • 復制代碼

    context線程類型的初始化


    這里我們再思考一下,如果我們將多個任務投放到一個線程那么他們是按照什么順序執(zhí)行的呢?這個答案需要分兩種情況解答,對于投放到FTaskThreadAnyThread執(zhí)行的任務會在創(chuàng)建的時候按照優(yōu)先級放到IncomingAnyThreadTasks數組里面,然后每次線程完成任務后會從這個數組里面彈出未執(zhí)行的任務來執(zhí)行,他的特點是我們有權利隨時修改和調整這個任務隊列。而對于投放到FNamedTaskThread執(zhí)行的任務,會被放到其本身維護的隊列里面,通過FThreadTaskQueue來處理執(zhí)行順序,一旦放到這個隊列里面,我們就無法隨意調整任務了。
    ?


    4.3 TaskGraph系統(tǒng)中的任務與事件

    雖然前面已經比較細致的描述了TaskGraph系統(tǒng)的框架,但是一個非常重要的特性我們還沒講到,就是任務依賴的實現原理。怎么理解任務依賴呢?簡單來說,就是一個任務的執(zhí)行可能依賴于多個事件對象,這些事件對象都觸發(fā)之后才會執(zhí)行這個任務。而這個任務完成后,又可能觸發(fā)其他事件,其他事件再進一步觸發(fā)其他任務,大概的效果是下圖這樣。
    ?

    任務與事件的依賴關系圖


    每個任務結束分別觸發(fā)一個事件,Task4需要等事件A、B都完成才會執(zhí)行,并且不會接著觸發(fā)其他事件。Task5需要等事件B、C都完成,并且會觸發(fā)事件D,D事件不會再觸發(fā)任何任務。當然,這些任務和事件可能在不同的線程上執(zhí)行。

    這里再看一下Task任務的創(chuàng)建代碼,分析一下先決依賴事件與后續(xù)等待事件都是如何產生的。
    ?

  • FGraphEventRef Join=TGraphTask<FVictoryTestTask>::CreateTask(NULL, ENamedThreads::GameThread).ConstructAndDispatchWhenReady();
  • 復制代碼


    CreateTask的第一個參數就是該任務依賴事件數組(這里為NULL),如果傳入一個事件數組的話,那么當前任務就會通過SetupPrereqs函數設置這些依賴事件,并且在所有依賴事件都觸發(fā)后再將該任務放到任務隊列里面分配給線程執(zhí)行。

    當執(zhí)行CreateTask時,會通過FGraphEvent::CreateGraphEvent()構建一個新的后續(xù)事件,再通過函數ConstructAndDispatchWhenReady返回。這樣我們就可以在當前的位置執(zhí)行
    ?

  • FTaskGraphInterface::Get().WaitUntilTaskCompletes(Join, ENamedThreads::GameThread_Local);
  • 復制代碼


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

    Task系統(tǒng)相關類圖


    4.4 其他相關技術細節(jié)

    1.FThreadSafeCounter

    通過調用不同平臺的原子操作來實現線程安全的計數
    ?

  • int32 Add( int32 Amount )
  • {
  • ? ? ? ? return FPlatformAtomics::InterlockedAdd(&Counter, Amount);
  • }
  • 復制代碼


    2. Task的構造方式

    我們看到相比AsyncTask,TaskGraph的創(chuàng)建可謂是既新奇又復雜,首先要調用靜態(tài)的CreateTask,然后又要通過返回值執(zhí)行ConstructAndDispatchWhenReady。那么這么做的目的是什么呢?按照我個人的理解,主要是為了能把想要的參數都傳進去。其實每創(chuàng)建一個任務,都需要傳入兩套參數,一套參數指定依賴事件,屬于任務系統(tǒng)的自身特點,另一套參數傳入玩家自定義任務的相關參數。為了實現這個效果,UE先通過工廠方法創(chuàng)建抽象任務把相關特性保存進去,然后通過內部的一個幫助類FConstructor構建一個真正的玩家定義的任務。如果C++玩的不溜,這樣的方法還真難想出來。(這是我個人猜測,如果你有更好的理解歡迎留言評論)

    3. FScopedEvent

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

    4. WaitUntilTaskCompletes的實現機制

    顧名思義,該函數的功能就是在任務結束之前保持當前線程的等待。不過他的實現確實很有趣,第一個參數是等待的事件Event,第二個參數是當前線程類型。如果當前的線程沒有任何Task,他會判斷傳入的事件數組是否都完成了,完成即可返回,沒有完成就會構建一個FReturnGraphTask類型的任務,然后執(zhí)行ProcessThreadUntilRequestReturn等所有的依賴事件都完成后才會返回。
    ?

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


    如果當前的線程有Task任務,他就創(chuàng)建一個ScopeEvent,并執(zhí)行TriggerEventWhenTasksComplete等待前面?zhèn)魅氲腡asks都完成后再返回。
    ?

  • FScopedEvent Event;
  • TriggerEventWhenTasksComplete(Event.Get(), Tasks, CurrentThreadIfKnown);
  • 復制代碼


    五.總結

    到這里,我們已經看到了三種使用多線程的方式,每種機制里面都有很多技術點值得我們深入學習。關于機制的選擇這里再給出一點建議:

    對于消耗大的,復雜的任務不建議使用TaskGraph,因為他會阻塞其他游戲線程的執(zhí)行。即使你不在那幾個有名字的線程上執(zhí)行,也可能會影響到游戲的其他邏輯。比如物理計算相關的任務就是在非指定名稱的線程上執(zhí)行的。這種復雜的任務,建議你自己繼承Runnable創(chuàng)建線程,或者使用AsynTask系統(tǒng)。

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

    另外,不要在非GameThread線程內執(zhí)行下面幾個操作:
    ?

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



    一開始我也不是很理解,所以就在其他線程里面執(zhí)行了Spawn操作,然后就蹦在了下面的地方。可以看到,SpawnActor的時候會執(zhí)行物理數據的初始化,而這個操作是必須要在主線程里面執(zhí)行的,我猜其他的位置肯定還有很多類似的宏。至于原因,我想就是我們最前面提到的“游戲不適合利用多線程優(yōu)化”,游戲GamePlay中各個部分非常依賴順序,多線程沒辦法很好的處理這些關系。再者,游戲邏輯如此復雜,你怎么做到避免“競爭條件”呢?到處加鎖么?我想那樣的話,游戲代碼就沒法看了吧。
    ?

    在其他線程Spawn導致崩潰


    最后,我們再來一張全家福吧~
    ?

    多線程系統(tǒng)類圖(完整) 與50位技術專家面對面20年技術見證,附贈技術全景圖

    總結

    以上是生活随笔為你收集整理的《Exploring in UE4》多线程机制详解[原理分析]的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。