国产av一二三区|日本不卡动作网站|黄色天天久久影片|99草成人免费在线视频|AV三级片成人电影在线|成年人aV不卡免费播放|日韩无码成人一级片视频|人人看人人玩开心色AV|人妻系列在线观看|亚洲av无码一区二区三区在线播放

網(wǎng)易首頁 > 網(wǎng)易號 > 正文 申請入駐

UE5多線程|TaskGraph

0
分享至


【USparkle專欄】如果你深懷絕技,愛“搞點研究”,樂于分享也博采眾長,我們期待你的加入,讓智慧的火花碰撞交織,讓知識的傳遞生生不息!

這是侑虎科技第1932篇文章,感謝作者南京周潤發(fā)供稿。歡迎轉(zhuǎn)發(fā)分享,未經(jīng)作者授權(quán)請勿轉(zhuǎn)載。如果您有任何獨到的見解或者發(fā)現(xiàn)也歡迎聯(lián)系我們,一起探討。(QQ群:793972859)

作者主頁:

https://www.zhihu.com/people/xu-chen-71-65

TaskGraph是線程池的進階,能讓任務(wù)之間產(chǎn)生依賴,上層可以方便地指定這種依賴。各任務(wù)的依賴關(guān)系就形成了“圖”。

除了線程池,TaskGraph還可以管理GrameThread、RenderThread等獨立線程的調(diào)度,是UE中最復(fù)雜,功能最全面的多線程調(diào)度框架了。

典型場景

UE的多線程GC是TaskGraph的一個典型場景,需要把一個大的Array分割成若干小的Array,然后分到多個線程處理,GameThread需要等這些線程都處理完了,再執(zhí)行以后的任務(wù)。代碼如下:


注意最后的ParallelFor,把多線程處理封裝成并行For行為,分發(fā)到多個線程,然后等待多線程執(zhí)行結(jié)束。

如果用普通線程實現(xiàn)這個功能,需要手動用FEvent實現(xiàn)等待,要寫一些特化代碼。

一、使用TaskGraph

1. Gamethread Tick

最常見的GameThread World Tick,就是由TaskGraph驅(qū)動的,因為GameThread也由TaskGraph管理,我們寫的Actor::Tick,Component::Tick都在這里執(zhí)行。Tick函數(shù)本身可以包裝到TGraphTask里,然后用WaitUntilTasksComplete函數(shù)執(zhí)行所有Task。


2. Async函數(shù)

Async函數(shù)可以指定EAsyncExecution::TaskGraph,讓任務(wù)在TaskGraph線程池中執(zhí)行。還能指定EAsyncExecution::TaskGraphMainThread,讓一些短時間任務(wù)在主線程執(zhí)行。


3. WaitUntilTasksComplete

如果需要發(fā)出一些異步任務(wù),然后等待執(zhí)行結(jié)束,可以手動構(gòu)造FGraphEventArray,然后調(diào)用WaitUntilTasksComplete等待執(zhí)行完畢,這里能體現(xiàn)TaskGraph的調(diào)度。


二、TaskGraph線程池

TaskGraph包含了線程池功能,不妨首先看線程池部分是如何實現(xiàn)的,這也比較好切入。類似FQueuedThreadPoolBase結(jié)構(gòu),TaskGraph的線程池有FTaskGraphInterface、FScheduler、FThread、TGraphTask和TAsyncGraphTask。

1. FTaskGraphInterface

FTaskGraphInterface是TaskGraph的管理類,是個單例,本身也是Interface,一些重要功能由子類實現(xiàn)。

接口

  • Startup:初始化TaskGraph。

  • Shutdown:關(guān)閉TaskGraph。

  • AttachToThread:把一個獨立線程添加到TaskGraph中,比如GameThread和RenderThread。

  • WaitUntilTasksComplete:讓一些線程運行若干任務(wù),并在當(dāng)前線程等待這些任務(wù)都執(zhí)行完。

  • TriggerEventWhenTasksComplete:當(dāng)若干任務(wù)執(zhí)行完,觸發(fā)一個Fevent。

  • ProcessThreadUntilIdle:讓一個NameThread一直處理自己的TaskQueue,直到執(zhí)行完所有Task。

子類

FTaskGraphCompatibilityImplementation

UE5的新TaskGraph子類,實現(xiàn)了TaskGraph的核心功能,不包含任務(wù)依賴功能,任務(wù)依賴由task實現(xiàn)。

成員

  • uint32 PerThreadIDTLSSlot:TaskGraph用FWorkerThread結(jié)構(gòu)體管理線程,每個線程在自己的TLS變量中存儲指向FWorkerThread結(jié)構(gòu)的指針。

  • Int32 NumNamedThreads:Named線程數(shù)量。

  • Int32 NumWorkerThreads:Worker線程數(shù)量。

  • Int32 NumBackgroundWorkers:BackgroudWorker數(shù)量。

  • Int32 NumForegroundWorkers:ForegroundWorker數(shù)量。

  • TArray NamedThreads:管理了所有NamedThread。

FTaskGraphImplementation:舊TaskGraph子類實現(xiàn),不看了。

2. FScheduler

FScheduler用于創(chuàng)建、管理Workder線程,以及把Task分派給Worker線程。

成員

  • TArray > WorkerThreads:工作線程。

  • TAlignedArray WorkerLocalQueues:WorkerThread對應(yīng)的Task。

  • TAlignedArray WorkerEvents:WorkerThread對應(yīng)的Event。

  • EThreadPriority WorkerPriority:工作線程優(yōu)先級。

  • EThreadPriority BackgroundPriority:Background WorkerThread優(yōu)先級。

  • FSchedulerTls::FQueueRegistry QueueRegistry:全局任務(wù)隊列。

方法

  • StartWorkers:創(chuàng)建WorkerThreads和Event等。

  • StopWorkers:執(zhí)行完所有Task,然后銷毀WorkerThreads。

  • TryLaunch:在WorkerThreads上執(zhí)行Task。

  • WakeUpWorker:通過Event Trigger喚醒WorkerThreads。

3. FThread

TaskGraph創(chuàng)建的WorkerThread,使用FThread來管理,它是操作系統(tǒng)中一個線程的表示,封裝了一個FThreadImpl。

方法

Join:最主要的方法,等待線程執(zhí)行完畢。

成員

TSharedPtr Impl:實際的Frunnable。

4. FThreadImpl

FThread的具體實現(xiàn),繼承自Frunnable。

方法

Run:調(diào)用了成員ThreadFunction。

成員

  • TUniqueFunction ThreadFunction:線程要執(zhí)行的函數(shù),就是WorkerMain。

  • TUniquePtr RunnableThread:對應(yīng)的FRunnableThread對象。

5. TGraphTask

TaskGraph系統(tǒng)中管理的Task,不直接調(diào)用用戶提供的Task函數(shù),而是把函數(shù)封裝成一個user defined task,存儲在其中。

成員

  • TAlignedBytes TaskStorage:存儲的user defined task,類型由模板指定。

  • FGraphEventRef Subsequents:存儲哪些GraphTask以我們?yōu)榍爸谩?/p>

方法

  • CreateTask:創(chuàng)建一個新GraphTask。

  • ExecuteTask:執(zhí)行Task。

  • SetupPrereqs:設(shè)置Task前置。

6. TAsyncGraphTask

屬于user defined task,是UE為實現(xiàn)Async函數(shù)而創(chuàng)建的類。

成員

  • TUniqueFunction Function:用戶提供的Task方法。

  • LowLevelTasks::FTask TaskHandle:FSchedule中對應(yīng)的FTask對象。

方法

DoTask:執(zhí)行Function。

7. FTask

Scheduler中使用的最底層任務(wù)對象。

成員

  • FTaskDelegate Runnable:封裝的Task函數(shù)對象。

  • FPackedDataAtomic PackedData:Priority,DebugName等信息。

方法

ExecuteTask:執(zhí)行Task。

借用其他博主畫的類圖,這張類圖畫的很好,但需要把其中的FTaskGraphImplementation類換成FTaskGraphCompatibilityImplementation:


三、初始化Worker線程

在PreInitPreStartupScreen函數(shù)中,會調(diào)用FTaskGraphInterface::Startup函數(shù)初始化TaskGraph,然后調(diào)用到Fscheduler::StartWorkers創(chuàng)建WorkerThreads。 參數(shù)NumberOfWorkerThreadsToSpawn與CPU核數(shù)有關(guān),Windows平臺為總核數(shù)減2,估計一個留給GameThread,一個留給RenderThread。




WorkerThreads分為ForegroundWorker和BackgroundWorker,線程優(yōu)先級不一樣,分別是TPri_SlightlyBelowNormal和TPri_BelowNormal,F(xiàn)oregroundWorker默認只有兩個。最終的創(chuàng)建WorkerThreads代碼如下:


對于每個WorkerThread,要創(chuàng)建三樣?xùn)|西:

  • 首先創(chuàng)建一個屬于該WorkerThread的FSleepEvent,內(nèi)部包含了WorkerThread當(dāng)前狀態(tài)和對應(yīng)的FEvent對象,用于管理WorkerThread的Sleep、Running等狀態(tài)轉(zhuǎn)換,存儲在WorkerEvents中。

  • 然后創(chuàng)建一個Local任務(wù)隊列,用于存儲Task,存在WorkerLocalQueues數(shù)組中。

  • 最后通過CreateWorker創(chuàng)建一個線程,用FThread包裝,存儲在WorkerThreads數(shù)組。線程函數(shù)是FScheduler::WorkerMain,主要任務(wù)從Task隊列中取出Task并執(zhí)行。

對于ForegroundWorker和BackgroundWorker,一些參數(shù)會有不同。

除了專門的WorkerThread,GameThread也能作為WorkerThread使用,可以把一些Task指定到GameThread執(zhí)行,具體會在下面介紹。

1. 添加任務(wù)

觀察Async函數(shù),首先調(diào)用CreateTask,創(chuàng)建一個FConstructor對象,內(nèi)部包裝一個TGraphTask實例。TGraphTask創(chuàng)建時可以指定前置Task,但Async函數(shù)的任務(wù)是輕量的異步任務(wù),沒有前置,因此這里直接用NULL。TGraphTask接受模板參數(shù)TTask,這里為TAsyncGraphTask。



TAsyncGraphTask

TAsyncGraphTask是用戶自定義Task,可以把一個Lambda函數(shù)派發(fā)到WorkerThread或者GameThread上執(zhí)行。

DoTask函數(shù)


GetDesiredThread函數(shù),可以在構(gòu)造函數(shù)中傳入想執(zhí)行的線程。


然后執(zhí)行ConstructAndDispatchWhenReady,先構(gòu)造一個TAsyncGraphTask實例,設(shè)置到TGraphTask.TaskStorage指針上。然后執(zhí)行Setup函數(shù),其中一些操作是GraphTask前置和后置相關(guān)的,先不管,最后會進入QueueTask函數(shù),把任務(wù)添加到TaskGraph執(zhí)行。


注意到這里用了FConstructor作為Helper類,把難寫的TaskStorage原地構(gòu)造包在里面,更易使用。

FConstructor還有另一個函數(shù)ConstructAndHold,這可以先創(chuàng)建TGraphTask,但不執(zhí)行,后面通過手動調(diào)用TGraphTask::Unlock執(zhí)行,但這種用法不多。


GraphTask也有一個優(yōu)先級類型,為ETaskPriority,這里首先會根據(jù)GraphTask希望執(zhí)行的線程類型,得到對應(yīng)的TaskPriority,AnyThread對應(yīng)的就是Normal。

Task->GetTaskHandle()獲取了GraphTask內(nèi)部的FTask對象,Init操作用于把Priority和封裝的Lambda函數(shù)參數(shù)賦值進去,初始化FTask對象。

最后TryLaunch會進入FSchedule,把FTask加入到任務(wù)隊列中。



任務(wù)隊列分為Thread Local和Global兩種,Async函數(shù)場景會加入Global,TaskGraph任務(wù)隊列特點是無鎖,即使多生產(chǎn)者,多消費者,也不需要加CriticalSection級別的鎖,只使用原子操作。關(guān)于無鎖任務(wù)隊列,會在下面專門介紹。

WakeUpWorker后面再看。

至此,用戶提供的Task已經(jīng)被加入到任務(wù)隊列。

2. 執(zhí)行任務(wù)

首先看創(chuàng)建Worker Trhead的線程函數(shù)WorkerMain:


參數(shù)含義:

  • WorkerEvent:線程對應(yīng)的SleepEvent,存在Scheduler數(shù)組中。

  • ExternalWorkerLocalQueue:存Task的LocalQueue,當(dāng)前WorkerThread獨占,存在Scheduler數(shù)組中。

  • WaitCycles:線程短等待的YieldCycles,不同WorkerThread會有些差異,避免大家一起執(zhí)行YieldCycles。

  • bPermitBackgroundWork:BackgroundWorker為true,F(xiàn)oregroundWorker為false。

然后是一個大While循環(huán),不斷從Task隊列中取Task執(zhí)行,沒有Task則進入Sleep。這里涉及到一些細節(jié),首先看到Worker隊列有很多種,然后線程也不是簡單的沒Task就進入Sleep,而是有更多狀態(tài)切換,以達到更好性能。


先忽略Task隊列的細節(jié),因為這涉及到無鎖隊列的實現(xiàn),認為從一個邏輯上的隊列里取Task,進入TryExecuteTaskFrom函數(shù)。最終進入ExecuteTask函數(shù),執(zhí)行用戶提供的Task,返回值A(chǔ)nyExecuted表示是否執(zhí)行了Task。


Task處理完后不直接用WaitEvent進入Wait,TaskGraph里增加了一個Drowsing(休眠)狀態(tài),總共有三個狀態(tài),狀態(tài)通過FSleepEvent結(jié)構(gòu)體維護,轉(zhuǎn)換邏輯在TrySleeping函數(shù)。

Running:正在執(zhí)行Task。

Drowsing:隊列中Task剛執(zhí)行完不久,執(zhí)行WorkerSpinCycles次的主動YieldCycles函數(shù),釋放一點CPU時間片,估計為了避免頻繁調(diào)用Wait和Trigger。進入Drowsing會把FSleepEvent加入SleepEventStack容器,認為已經(jīng)處于不活躍狀態(tài),需要通過WakeUpWorker調(diào)用從容器中移除,改回Running。

Sleeping:一段時間的Drowsing狀態(tài)內(nèi)沒有執(zhí)行新的Task,調(diào)用FEvent.Wait,線程進入阻塞狀態(tài)。只有通過WakeUpWorker函數(shù)執(zhí)行FEvent.Trigger后才能恢復(fù)執(zhí)行,同時會把FSleepEvent從SleepEventStack中彈出,把狀態(tài)改回Running。

狀態(tài)轉(zhuǎn)換圖如下:


3. Task優(yōu)先級

游戲運行過程中會產(chǎn)生大量Task,UE支持為Task指定多個優(yōu)先級,提供更細粒度的控制,雖然在Async函數(shù)里只提供了一種優(yōu)先級。這里只討論Task在WorkerThread中執(zhí)行的情況,GameThread和RenderThread執(zhí)行Task另外再討論。

Task優(yōu)先級定義如下:


真正有意義的是High、Normal、BackgroundHigh、BackgroundNormal和BackgroundLow五種,運行時會按照優(yōu)先級維護多個隊列,按照優(yōu)先級順序執(zhí)行這些Task。

但用戶不能直接指定Task的優(yōu)先級。用戶自定義Task可以通過GetDesiredThread函數(shù)指定希望執(zhí)行的線程、線程優(yōu)先級、以及Background Task的優(yōu)先級,最終會設(shè)置在TGraphTask的ThreadToExecuteOn屬性上。

這個int32中嵌入了很多信息:


ENamedThreads的組成如下,按比特位劃分了不同區(qū)域,具體也可看enum定義,這里過長不貼了。


  • ThreadId部分8位

標識線程的ID,NamedThread下標從0開始,StatsThread=0,RHIThread=1,AudioThread=2,GameThread=3,AnyThread=0xff。

  • QueueIndex部分1位

MainQueue=1,LocalQueue=2。

  • ThreadPriority部分2位

指定不同線程優(yōu)先級,也可以認為是Task的粗粒度優(yōu)先級,NormalThreadPriority=0,HighThreadPriority=1,BackgroundThreadPriority=2。

  • TaskPriority部分1位

用戶定義的Task細粒度優(yōu)先級,僅對ThreadPriority=BackgroundThreadPriority時有效,把BackgroundThreadPriority再細分,NormalTaskPriority=0,HighTaskPriority=1。

注意ThreadId的AnyThread選項,表示在任意Worker線程執(zhí)行,但之前介紹過Worker線程分為ForgroundWorker和BackgroundWorker,它們線程優(yōu)先級不同,Task具體在哪類Worker中執(zhí)行,還是要看根據(jù)ENamedThreads得到的TaskPriority。

多個枚舉可以組合,引擎提供了一些預(yù)置enum,目前并不是所有組合都支持,比如AnyHiPriThreadNormalTask和AnyHiPriThreadHiPriTask是等同的,只是先都定義了。

以AnyBackgroundThreadNormalTask為例,該Task會在WorkerThread中執(zhí)行,線程TaskPriority是BackgroundNormal,用戶定義TaskPriority是NormalTaskPriority。


UE也提供了一些Helper函數(shù),從中獲取信息:

  • GetThreadIndex

  • GetQueueIndex

  • GetTaskPriority

  • GetThreadPriorityIndex

最終的TaskPriority和WorkerThread種類由ThreadPriority和用用戶定義TaskPriority共同決定,代碼在FTaskGraphCompatibilityImplementation::QueueTask中,整理的對應(yīng)關(guān)系如下:


TaskQueue也按照TaskPriority數(shù)量進行了劃分,各優(yōu)先級有自己的容器。TaskQueue分為Thread Local LocalQueue和全局的OverflowQueues,定義如下,是個ETaskPriority::Count的數(shù)組:


以O(shè)verflowQueues為例,添加Task代碼如下:


取Task代碼如下,優(yōu)先級從高到低遍歷:


總結(jié)一下,TaskGraph提供線程池功能時執(zhí)行流程圖如下,這里TAsyncGraphTask也可以換成我們自己寫的用戶Task,同樣使用TGraphTask ::CreateTask().ConstructAndDispatchWhenReady接口即可。


四、TaskGraph管理NamedThread

TaskGraph不僅可以創(chuàng)建WorkerThread執(zhí)行任務(wù),還能把GameThread、RenderThread等專用線程也納入管理,分派任務(wù)給線程執(zhí)行。

回顧FTaskGraphCompatibilityImplementation定義,其中包含了NameThreads容器,用一個FWorkerThread代表一個NamedThread。


NamedThread線程ID定義如下,有RHIThread、AudioThrad、GameThread和RenderThread四個。


1. FWorkerThread

表示一個線程,包含相關(guān)信息,目前實現(xiàn)只用于NamedThread。

成員

  • FTaskThreadBase*TaskGraphWorker:真正的TaskGraphWorker。

  • bool bAttached:NameThread是否被注冊到TaskGraph系統(tǒng)。

2. FTaskThreadBase

用于讓NamedThread有執(zhí)行GraphTask的能力。

成員

  • ENamedThreads::Type ThreadId:線程ID。

  • Uint32 PerThreadIDTLSSlot:FWorkerThread對象指針會被存儲到這個Slot對應(yīng)的TLS中,這樣NamedThread就能取到它了。

  • TArray NewTasks:這個線程要執(zhí)行的Task。

  • FWorkerThread*OwnerWorker:所有者FWorkerThread的指針。

函數(shù)

ProcessTasksUntilQuit

  • ProcessTasksUntilIdle:兩個都用于讓NameThreads不斷執(zhí)行Task,直到線程Idle或者設(shè)置RequestQuit標記。

  • EnqueueFromThisThread:向線程添加GraphTask任務(wù),當(dāng)前執(zhí)行的線程就是NamedThread。

  • EnqueueFromOtherThread:效果同上,當(dāng)前執(zhí)行線程不是NamedThread。

  • Run:內(nèi)部執(zhí)行ProcessTasksUntilQuit。

3. FNamedTaskThread

繼承自FTaskThreadBase,用于管理NamedTask。

成員

FThreadTaskQueue Queues[ENamedThreads::NumQueues]:存儲Task的隊列,分MainQueue和LocalQueue兩個。

函數(shù)

覆寫了ProcessTasksUntilQuit,ProcessTasksUntilIdle,EnqueueFromOtherThread。

4. FThreadTaskQueue

NamedTaskThread擁有的Task隊列。

  • FStallingTaskQueue StallQueue:包裝了兩個LockFreelist,對應(yīng)High和Normal兩個優(yōu)先級,NamedThread的Task只有這兩個優(yōu)先級。

  • FEvent*StallRestartEvent:當(dāng)線程執(zhí)行完Task后,在該Event上等待

五、創(chuàng)建FWorkerThread對象

在TaskGraph Startup時,會根據(jù)NameThreads數(shù)量,創(chuàng)建對應(yīng)的FWorkerThread對象,存儲在NamedThreads數(shù)組中。FWorkerThread初始化主要有兩個參數(shù):一個是分配的TLS Slot,用來存它,另一個是FNamedTaskThread對象。


六、GameThread注冊到TaskGraph

當(dāng)前線程調(diào)用AttachToThread函數(shù)可以把自己注冊到TaskGraph中,需要提供一個線程ID。

這是GameThread的注冊方式,在Startup后就立即注冊了:


接著執(zhí)行到這里,先根據(jù)CurrentThread ID獲取到對應(yīng)的TaskGraphWorker,然后調(diào)用InitializeForCurrentThread,該函數(shù)會把OwnerWorker存儲在PerThreadIDTLSSlot的TLS中。



這樣就完成了注冊。

其他幾個NamedThread也用同樣的方式注冊。

七、向NameThread添加Task任務(wù)

使用Async函數(shù)可以向GameThread添加Task,把參數(shù)設(shè)為EAsyncExecution::TaskGraphMainThread即可。往后的CreateTask等流程都相同,區(qū)別只在最后的QueueTask。


這里傳入的InThreadToExecuteOn為GameThread,InCurrentThreadIfKnown沒有設(shè)置,默認為AnyThread,也可以工作。

QueueToExecuteOn表示希望加在MainQueue還是LocalQueue,在外部可以設(shè)置。

比較值得注意的GetCurrentThread函數(shù),需要得到當(dāng)前線程ID,用ENamedThreads表示。


如果是NamedThread,已經(jīng)設(shè)置了TLS,從中取出FWorkerThread指針,然后得到在NamedThreads中的偏移,就是ThreadId。

如果是AnyThread,還會先嘗試獲取當(dāng)前線程上的ActiveTask,然后獲取ThreadPriority和TaskPriority,一并返回。

最后根據(jù)ThreadToExecuteOn和CurrentThreadId,調(diào)用EnqueueFromThisThread或EnqueueFromOtherThread,這兩個接口區(qū)別為前者是當(dāng)前線程調(diào)用的,后者可以由其他線程調(diào)用,也可以由當(dāng)前線程調(diào)用,多了一步線程喚醒操作。

EnqueueFromThisThread把Task加到Queues容器中,QueueIndex決定是MainQueue還是LocalQueue,默認MainQueue,然后從之前的ThreadIdAndIndex里獲取到TaskPriority,決定加到內(nèi)部的HighPriority還是NormalPriority Task容器。


EnqueueFromOtherThread也會先把Task加入StallQueue,然后看是否有ThreadToStart,有則調(diào)用Trigger,喚醒線程。


八、NamedThread執(zhí)行Task

以GameThread為例,看如何執(zhí)行TaskGraph中的Task。

GameThread每幀都會通過World::Tick函數(shù),執(zhí)行各種Actor的Tick,驅(qū)動游戲世界,而各種Tick函數(shù)又通過FTickTaskManager管理,背后再轉(zhuǎn)換成一個個TGraphTask,放到TaskGraph中執(zhí)行。

直接進入FTickTaskSequencer::ReleaseTickGroup函數(shù),這里會執(zhí)行一個TickGroup中全部的Tick,代碼如下:


然后進入WaitUntilTasksComplete函數(shù),執(zhí)行這些Task。WaitUntilTasksComplete含義是等待這些Task執(zhí)行完,方法為創(chuàng)建一個FReturnGraphTask,并把要等待的Task設(shè)為前置,F(xiàn)ReturnGraphTask作用是把FNamedTaskThread.Queue.QuitForReturn設(shè)為true,讓TaskGraph執(zhí)行完這些Task后就返回。

WaitUntilTasksComplete


之后執(zhí)行到ProcessTasksUntilQuit和ProcessTasksNamedThread,不斷從Queue中取GraphTask并執(zhí)行,直到執(zhí)行了FReturnGraphTask,然后返回。



我們之前通過Async函數(shù)向GameThread添加的Task,也是在這里從Queue中取出,然后被執(zhí)行的。

再借用一張圖,描述NamedThreads執(zhí)行Task的過程:


九、GraphTask的依賴關(guān)系

TaskGraph區(qū)別于普通線程池的一大特點,就是GraphTask能存在前置依賴,這樣可以自定義Task的執(zhí)行順序,多線程動畫、多線程GC等都是這樣實現(xiàn)的。

GraphTask依賴關(guān)系需要解決兩個問題:

  • 如何組織Task,按照依賴順序執(zhí)行這些Task;

  • 等待依賴的Task執(zhí)行完成會可能造成線程休眠,如何喚醒線程。

以多線程動畫更新為示例,看如何建立Task間依賴。動畫多線程更新可以把動畫的Update、Evaluate開銷都放到WorkerThread中,減輕GameThread負擔(dān),當(dāng)SkeletalMeshComponent多時尤為明顯。


首先創(chuàng)建一個FParallelAnimationEvaluationTask,用來做動畫多線程Update和Evaluate,派發(fā)到WorkerThread上執(zhí)行。然后創(chuàng)建一個FParallelAnimationCompletionTask,用來做動畫更新后的PostAnimEvaluation,在GameThread上執(zhí)行,前置為FParallelAnimationEvaluationTask,這一切都發(fā)生在PrePhysics tick階段。

簡單時序圖如下:


1. GraphEvent

這里Task依賴通過FGraphEventArray結(jié)構(gòu)實現(xiàn),而FGraphEventArray其實是一組FGraphEvent的引用,F(xiàn)GraphEvent是Task依賴的關(guān)鍵。


GraphEvent可以理解為GraphTask相關(guān)的“事件”,GraphTask之間通過“事件”聯(lián)系。

2. FGraphEvent

包含了一系列后置Task,該GraphEvent是它們的觸發(fā)條件。

成員

  • TClosableLockFreePointerListUnorderedSingleConsumer SubsequentList:后置Task,是無鎖鏈表。

  • FGraphEventArray EventsToWaitFor:該GraphEvent要等待的其他GraphEvent數(shù)組,其實只有一個元素,其他GraphEvent完成后,該EventGraph才會觸發(fā),在DontCompleteUntil里設(shè)置。

方法

  • AddSubsequent:添加一個后置Task。

  • DontCompleteUntil:提供一個前置GraphEvent,前置完成后自己才觸發(fā)。

回顧一下TGraphTask的成員:

  • Subsequents:該GraphTask對應(yīng)的FGraphEvent。

  • NumberOfPrerequistitesOutstanding:該GraphTask有多少個前置待執(zhí)行。

  • ConstructAndDispatchWhenReady函數(shù)會返回GraphTask對應(yīng)的GraphEvent,外部就能操作它了。

3. CreateTask

CreateTask方法可以接受Prerequistes參數(shù),得到該GraphTask的前置,接著進入TGraphTask::SetupPrereqs函數(shù)。



會通過AddSubsequent函數(shù)把自己添加到所有Prerequisties的后置里,然后會判斷Prerequisties是否都完成了,完成后才通過QueueTask把該GraphTask加到Task隊列里,等待執(zhí)行,大部分情況都不會進入,需要等待前置。

4. DispatchSubsequents

在TGraphTask執(zhí)行完后,會通過Subsequents對象執(zhí)行DispatchSubsequents,讓其他依賴自己的Task執(zhí)行。這里要分有無EventsToWaitFor的情況。

無EventsToWaitFor:

TGraphTask執(zhí)行完后,就立即觸發(fā)完成事件,需要遍歷所有SubsequentList里的后置Task,調(diào)用ConditionalQueueTask,如果后置的所有前置都已被觸發(fā),就調(diào)用QueueTask,把自己加入Task隊列,等待執(zhí)行。



有EventsToWaitFor:

有時候TGraphTask自己完成了,但不想立即觸發(fā)事件,還想等待另一個GraphEvent完成后再觸發(fā),

比如多線程動畫更新里的TickFunction函數(shù),對應(yīng)的事件要等到TickCompletionEvent完成后再觸發(fā)。相當(dāng)于TickFunction Task已經(jīng)在執(zhí)行了,但還想給它添加前置一樣。



這個操作通過增加一個NullGraphTask完成,這個Task繼承了自己的Subsequents,并且把EventsToWaitFor作為自己前置,本身的ExecuteTask并沒有任何邏輯,只是為了觸發(fā)原本的后置Task。


回到動畫多線程更新的例子,用圖表展示執(zhí)行流程和GraphTask、GraphEvent的工作過程:


這只是簡單的TaskGraph依賴關(guān)系,當(dāng)然可以自己組合出一些多前置,多后置的TaskGraph依賴,背后原理是一樣的。

十、NamedThread Sleep/喚醒

多線程動畫例子中,如果FParallelAnimationEvaluationTask執(zhí)行時間過長,GameThread已經(jīng)把PrePhysics階段的所有Tick都執(zhí)行完了,就會進入Sleep狀態(tài),等FParallelAnimationEvaluationTask執(zhí)行完后再喚醒GameThread繼續(xù)執(zhí)行。

1. 進入Sleep

GameThread在Tick時會執(zhí)行ProcessTasksNamedThread,While循環(huán)從Queue中獲取下一個Task,執(zhí)行到ReturnTask之前都不會退出,如果取不到Task了,說明需要等其他線程執(zhí)行完前置Task,那么GameThrad自身會在這個Queue的StallRestartEvent上Wait,進入Sleep狀態(tài)。


StallQueue有設(shè)計,可以用一個uint64記錄線程是否在StallRestartEvent上Wait,目前支持一個線程,因為StallQueue也是單個FNamedTaskThread對象獨有的,但看代碼是想設(shè)計成支持26個線程。

看下StallQueue的Pop函數(shù):


當(dāng)沒能獲取到新的Task時,表示當(dāng)前Thread要進入Wait了,會修改MasterState,記錄下這個線程。MasterState是一個巧妙的uint64位結(jié)構(gòu),可以同時記錄多線程訪問信息和等待的線程信息,結(jié)構(gòu)如下:


Counter用于多線程保護,每次進Pop和Push都會加1,在修改Ptrs前都會比較一下Counter是否和進函數(shù)時相同,防止Pop和Push在不同線程被執(zhí)行,導(dǎo)致判斷不正確。

當(dāng)Counter判斷通過,就會把Ptrs的MyThread位設(shè)置為1,表示這個線程在StallRestartEvent上Wait了,目前MyThread固定為0。

2. 喚醒

當(dāng)調(diào)用EnqueueFromOtherThread添加Task后,會判斷線程是否在Sleep狀態(tài),然后執(zhí)行StallRestartEvent.Trigger()喚醒線程,繼續(xù)執(zhí)行。


StallQueue的Push函數(shù)如下:


會從MasterState中尋找Ptrs里被設(shè)置為1的位,表示哪些線程在上面Wait,得到ThreadToWake,外層函數(shù)再對其調(diào)用Trigger喚醒。

十一、一些Task同步函數(shù)

當(dāng)發(fā)出多個Task,分派到不同線程執(zhí)行后,邏輯上通常希望能對這些Task做些同步操作,比如在一個時間點等待這些Task都執(zhí)行完,或者像動畫多線程例子那樣給TickFunction加WaitEvent,TaskGraph框架提供了多種這樣的函數(shù)。

1. TaskGraph接口

  • WaitUntilTasksComplete(Tasks)

等待多個GraphEvent執(zhí)行完,內(nèi)部做法是增加一個FReturnTask,把傳入的Tasks作為其前置,然后調(diào)用ProcessThreadUntilRequestReturn。

比如如下代碼:



  • ProcessThreadUntilIdle

在NamedThread上調(diào)用,阻塞執(zhí)行當(dāng)前Queue里的所有Task,直到完成。

  • ProcessThreadUntilRequestReturn

與ProcessThreadUntilIdle類似,只是需要預(yù)先添加一個ReturnTask任務(wù)。

ProcessThreadUntilIdle和ProcessThreadUntilRequestReturn兩個函數(shù)通常只有引擎會使用,項目代碼里感覺沒這個需求。

2. GraphEvent接口

  • DontCompleteUntil

GraphEvent的函數(shù),之前動畫藍圖例子已介紹過,會給當(dāng)前GraphEvent設(shè)置另一個Event作為EventsToWaitFor,在EventsToWaitFor觸發(fā)后,才觸發(fā)當(dāng)前的GraphEvent。

流程圖見上面。

  • Wait

內(nèi)部調(diào)用了TaskGraph的WaitUntilTasksComplete接口,把自己作為參數(shù)傳入,效果與WaitUntilTasksComplete相同。

文末,再次感謝南京周潤發(fā) 的分享, 作者主頁:https://www.zhihu.com/people/xu-chen-71-65, 如果您有任何獨到的見解或者發(fā)現(xiàn)也歡迎聯(lián)系我們,一起探討。(QQ群: 793972859 )。


近期精彩回顧




特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相關(guān)推薦
熱點推薦
世錦賽戰(zhàn)報:組合球一炮雙響,希金斯三度反超被扳平了8-8墨菲

世錦賽戰(zhàn)報:組合球一炮雙響,希金斯三度反超被扳平了8-8墨菲

求球不落諦
2026-05-01 20:32:05
定價僅299刀!蘋果全新Mac mini Neo來了,或改寫PC市場格局

定價僅299刀!蘋果全新Mac mini Neo來了,或改寫PC市場格局

叮當(dāng)當(dāng)科技
2026-04-30 13:22:13
第二個“恒大”出現(xiàn)!年收入超6000億,老板與許家印“稱兄道弟”

第二個“恒大”出現(xiàn)!年收入超6000億,老板與許家印“稱兄道弟”

億通電子游戲
2026-04-30 17:20:09
一場2-6落敗不算啥,可怕的是艾倫的發(fā)聲:進攻型選手最難對付

一場2-6落敗不算啥,可怕的是艾倫的發(fā)聲:進攻型選手最難對付

林子說事
2026-05-01 15:49:35
時隔45年,中國第二輪嚴打重啟,目標改變總體戰(zhàn)打響

時隔45年,中國第二輪嚴打重啟,目標改變總體戰(zhàn)打響

裝滿幸福
2026-04-30 20:04:57
張?zhí)m連夜飛回北京照顧孫子,說出了五一假期的行程安排是合格奶奶

張?zhí)m連夜飛回北京照顧孫子,說出了五一假期的行程安排是合格奶奶

一盅情懷
2026-05-01 16:25:20
伊朗總統(tǒng):持續(xù)推進海上封鎖的行為不可容忍

伊朗總統(tǒng):持續(xù)推進海上封鎖的行為不可容忍

每日經(jīng)濟新聞
2026-05-01 09:16:47
蘋果煮水太養(yǎng)人!5種搭配換著喝,脾胃好臉色更紅潤

蘋果煮水太養(yǎng)人!5種搭配換著喝,脾胃好臉色更紅潤

開心美食白科
2026-04-11 15:27:36
馬科斯怎么都想不到!在仁愛礁坐灘27年的破船,卻意外助力了中國

馬科斯怎么都想不到!在仁愛礁坐灘27年的破船,卻意外助力了中國

蜉蝣說
2026-04-29 17:04:07
局勢惡化,中日又爆發(fā)爭端,日本登上釣魚島,解放軍戰(zhàn)艦果斷合圍

局勢惡化,中日又爆發(fā)爭端,日本登上釣魚島,解放軍戰(zhàn)艦果斷合圍

老黯談娛
2026-05-01 11:59:01
天津津門虎2-2武漢三鎮(zhèn),賽后評分出爐:天津津門虎20號第一

天津津門虎2-2武漢三鎮(zhèn),賽后評分出爐:天津津門虎20號第一

側(cè)身凌空斬
2026-05-01 21:41:42
安徽18歲男子蹊蹺失聯(lián) 警方通報

安徽18歲男子蹊蹺失聯(lián) 警方通報

黃河新聞網(wǎng)呂梁
2026-05-01 18:20:55
印度:我們每家都有電視,中國行嗎?法國:人家中國居民不用偷電

印度:我們每家都有電視,中國行嗎?法國:人家中國居民不用偷電

離離言幾許
2026-04-10 14:51:25
原來就是她騙了朱丹1600萬,她讓朱丹傾家蕩產(chǎn),朱丹讓她全網(wǎng)社死

原來就是她騙了朱丹1600萬,她讓朱丹傾家蕩產(chǎn),朱丹讓她全網(wǎng)社死

青橘罐頭
2026-03-19 17:06:28
曾是一代歌王,卻淪為人盡皆知的臺獨分子,如今家破人亡下場凄涼

曾是一代歌王,卻淪為人盡皆知的臺獨分子,如今家破人亡下場凄涼

君笙的拂兮
2026-05-01 12:50:32
人性殘忍法則:別人尊不尊重你,從來不看你付出多少,也不看你為人多好,而是看你身上有沒有這兩樣?xùn)|西

人性殘忍法則:別人尊不尊重你,從來不看你付出多少,也不看你為人多好,而是看你身上有沒有這兩樣?xùn)|西

心理觀察局
2026-05-01 09:52:15
她是留美碩士,后官至副國級,享年106歲,曾以八個字評價毛主席

她是留美碩士,后官至副國級,享年106歲,曾以八個字評價毛主席

興趣知識
2026-05-01 19:49:37
蘇州一對情侶,談了7年,女子提了18次分手,分手后在街頭痛哭!

蘇州一對情侶,談了7年,女子提了18次分手,分手后在街頭痛哭!

川渝視覺
2026-04-17 22:13:14
角色反轉(zhuǎn),聯(lián)大主席貝爾伯克訪華露底牌,目標直指中國一票否決權(quán)

角色反轉(zhuǎn),聯(lián)大主席貝爾伯克訪華露底牌,目標直指中國一票否決權(quán)

遁走的兩輪
2026-05-01 03:03:07
李小萌王雷豪華住宅被曝光,誰能想到王雷的煙成了最大亮點!

李小萌王雷豪華住宅被曝光,誰能想到王雷的煙成了最大亮點!

TVB的四小花
2026-05-01 20:26:24
2026-05-01 23:31:00
侑虎科技UWA incentive-icons
侑虎科技UWA
游戲/VR性能優(yōu)化平臺
1571文章數(shù) 987關(guān)注度
往期回顧 全部

科技要聞

DeepSeek發(fā)布多模態(tài)論文又連夜刪除

頭條要聞

8歲女孩跟隨鄰居長江游泳溺亡 鄰居曾抓住她又脫手

頭條要聞

8歲女孩跟隨鄰居長江游泳溺亡 鄰居曾抓住她又脫手

體育要聞

無奈!約基奇:這要在塞爾維亞 全隊早被炒了

娛樂要聞

馬筱梅產(chǎn)后身材恢復(fù)超好 現(xiàn)身戶外直播

財經(jīng)要聞

GPU神話松動,AI真正的戰(zhàn)場變了

汽車要聞

限時9.67萬起 吉利星越L/星瑞i-HEV智擎混動上市

態(tài)度原創(chuàng)

健康
家居
教育
游戲
公開課

干細胞治燒燙傷面臨這些“瓶頸”

家居要聞

靈動實用 生活藝術(shù)場

教育要聞

五一首日熱度爆棚!秦淮區(qū)中招會傳來最新消息!

LPL第二賽段:打破魔咒,讓一追二,IG三局戰(zhàn)勝WBG

公開課

李玫瑾:為什么性格比能力更重要?

無障礙瀏覽 進入關(guān)懷版