博客 / 詳情

返回

UE5多線程|TaskGraph

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


TaskGraph是線程池的進階,能讓任務之間產生依賴,上層可以方便地指定這種依賴。各任務的依賴關係就形成了“圖”。

除了線程池,TaskGraph還可以管理GrameThread、RenderThread等獨立線程的調度,是UE中最複雜,功能最全面的多線程調度框架了。

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

注意最後的ParallelFor,把多線程處理封裝成並行For行為,分發到多個線程,然後等待多線程執行結束。

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

一、使用TaskGraph

1. Gamethread Tick
最常見的GameThread World Tick,就是由TaskGraph驅動的,因為GameThread也由TaskGraph管理,我們寫的Actor::Tick,Component::Tick都在這裏執行。Tick函數本身可以包裝到TGraphTask裏,然後用WaitUntilTasksComplete函數執行所有Task。

2. Async函數
Async函數可以指定EAsyncExecution::TaskGraph,讓任務在TaskGraph線程池中執行。還能指定EAsyncExecution::TaskGraphMainThread,讓一些短時間任務在主線程執行。

3. WaitUntilTasksComplete
如果需要發出一些異步任務,然後等待執行結束,可以手動構造FGraphEventArray,然後調用WaitUntilTasksComplete等待執行完畢,這裏能體現TaskGraph的調度。

二、TaskGraph線程池

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

1. FTaskGraphInterface
FTaskGraphInterface是TaskGraph的管理類,是個單例,本身也是Interface,一些重要功能由子類實現。

接口

  • Startup:初始化TaskGraph。
  • Shutdown:關閉TaskGraph。
  • AttachToThread:把一個獨立線程添加到TaskGraph中,比如GameThread和RenderThread。
  • WaitUntilTasksComplete:讓一些線程運行若干任務,並在當前線程等待這些任務都執行完。
  • TriggerEventWhenTasksComplete:當若干任務執行完,觸發一個Fevent。
  • ProcessThreadUntilIdle:讓一個NameThread一直處理自己的TaskQueue,直到執行完所有Task。

子類
FTaskGraphCompatibilityImplementation
UE5的新TaskGraph子類,實現了TaskGraph的核心功能,不包含任務依賴功能,任務依賴由task實現。

成員

  • uint32 PerThreadIDTLSSlot:TaskGraph用FWorkerThread結構體管理線程,每個線程在自己的TLS變量中存儲指向FWorkerThread結構的指針。
  • Int32 NumNamedThreads:Named線程數量。
  • Int32 NumWorkerThreads:Worker線程數量。
  • Int32 NumBackgroundWorkers:BackgroudWorker數量。
  • Int32 NumForegroundWorkers:ForegroundWorker數量。
  • TArray<FWorkerThread> NamedThreads:管理了所有NamedThread。

FTaskGraphImplementation:舊TaskGraph子類實現,不看了。

2. FScheduler
FScheduler用於創建、管理Workder線程,以及把Task分派給Worker線程。

成員

  • TArray<TUniquePtr<FThread>> WorkerThreads:工作線程。
  • TAlignedArray<FSchedulerTls::FLocalQueueType> WorkerLocalQueues:WorkerThread對應的Task。
  • TAlignedArray<FSleepEvent> WorkerEvents:WorkerThread對應的Event。
  • EThreadPriority WorkerPriority:工作線程優先級。
  • EThreadPriority BackgroundPriority:Background WorkerThread優先級。
  • FSchedulerTls::FQueueRegistry QueueRegistry:全局任務隊列。

方法

  • StartWorkers:創建WorkerThreads和Event等。
  • StopWorkers:執行完所有Task,然後銷燬WorkerThreads。
  • TryLaunch:在WorkerThreads上執行Task。
  • WakeUpWorker:通過Event Trigger喚醒WorkerThreads。

3. FThread
TaskGraph創建的WorkerThread,使用FThread來管理,它是操作系統中一個線程的表示,封裝了一個FThreadImpl。

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

成員
TSharedPtr<class FThreadImpl, ESPMode::ThreadSafe> Impl:實際的Frunnable。

4. FThreadImpl
FThread的具體實現,繼承自Frunnable。

方法
Run:調用了成員ThreadFunction。

成員

  • TUniqueFunction<void()> ThreadFunction:線程要執行的函數,就是WorkerMain。
  • TUniquePtr<FRunnableThread> RunnableThread:對應的FRunnableThread對象。

5. TGraphTask
TaskGraph系統中管理的Task,不直接調用用户提供的Task函數,而是把函數封裝成一個user defined task,存儲在其中。

成員

  • TAlignedBytes<sizeof(TTask),alignof(TTask)> TaskStorage:存儲的user defined task,類型由模板指定。
  • FGraphEventRef Subsequents:存儲哪些GraphTask以我們為前置。

方法

  • CreateTask:創建一個新GraphTask。
  • ExecuteTask:執行Task。
  • SetupPrereqs:設置Task前置。

6. TAsyncGraphTask
屬於user defined task,是UE為實現Async函數而創建的類。

成員

  • TUniqueFunction<ResultType()> Function:用户提供的Task方法。
  • LowLevelTasks::FTask TaskHandle:FSchedule中對應的FTask對象。

方法
DoTask:執行Function。

7. FTask
Scheduler中使用的最底層任務對象。

成員

  • FTaskDelegate Runnable:封裝的Task函數對象。
  • FPackedDataAtomic PackedData:Priority,DebugName等信息。

方法
ExecuteTask:執行Task。

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

三、初始化Worker線程

在PreInitPreStartupScreen函數中,會調用FTaskGraphInterface::Startup函數初始化TaskGraph,然後調用到Fscheduler::StartWorkers創建WorkerThreads。 參數NumberOfWorkerThreadsToSpawn與CPU核數有關,Windows平台為總核數減2,估計一個留給GameThread,一個留給RenderThread。

WorkerThreads分為ForegroundWorker和BackgroundWorker,線程優先級不一樣,分別是TPri_SlightlyBelowNormal和TPri_BelowNormal,ForegroundWorker默認只有兩個。最終的創建WorkerThreads代碼如下:

對於每個WorkerThread,要創建三樣東西:

  • 首先創建一個屬於該WorkerThread的FSleepEvent,內部包含了WorkerThread當前狀態和對應的FEvent對象,用於管理WorkerThread的Sleep、Running等狀態轉換,存儲在WorkerEvents中。
  • 然後創建一個Local任務隊列,用於存儲Task,存在WorkerLocalQueues數組中。
  • 最後通過CreateWorker創建一個線程,用FThread包裝,存儲在WorkerThreads數組。線程函數是FScheduler::WorkerMain,主要任務從Task隊列中取出Task並執行。

對於ForegroundWorker和BackgroundWorker,一些參數會有不同。

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

1. 添加任務
觀察Async函數,首先調用CreateTask,創建一個FConstructor對象,內部包裝一個TGraphTask實例。TGraphTask創建時可以指定前置Task,但Async函數的任務是輕量的異步任務,沒有前置,因此這裏直接用NULL。TGraphTask接受模板參數TTask,這裏為TAsyncGraphTask。

TAsyncGraphTask
TAsyncGraphTask是用户自定義Task,可以把一個Lambda函數派發到WorkerThread或者GameThread上執行。

DoTask函數

GetDesiredThread函數,可以在構造函數中傳入想執行的線程。

然後執行ConstructAndDispatchWhenReady,先構造一個TAsyncGraphTask實例,設置到TGraphTask.TaskStorage指針上。然後執行Setup函數,其中一些操作是GraphTask前置和後置相關的,先不管,最後會進入QueueTask函數,把任務添加到TaskGraph執行。

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

FConstructor還有另一個函數ConstructAndHold,這可以先創建TGraphTask,但不執行,後面通過手動調用TGraphTask::Unlock執行,但這種用法不多。

GraphTask也有一個優先級類型,為ETaskPriority,這裏首先會根據GraphTask希望執行的線程類型,得到對應的TaskPriority,AnyThread對應的就是Normal。

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

最後TryLaunch會進入FSchedule,把FTask加入到任務隊列中。

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

WakeUpWorker後面再看。

至此,用户提供的Task已經被加入到任務隊列。

2. 執行任務
首先看創建Worker Trhead的線程函數WorkerMain:

參數含義:

  • WorkerEvent:線程對應的SleepEvent,存在Scheduler數組中。
  • ExternalWorkerLocalQueue:存Task的LocalQueue,當前WorkerThread獨佔,存在Scheduler數組中。
  • WaitCycles:線程短等待的YieldCycles,不同WorkerThread會有些差異,避免大家一起執行YieldCycles。
  • bPermitBackgroundWork:BackgroundWorker為true,ForegroundWorker為false。

然後是一個大While循環,不斷從Task隊列中取Task執行,沒有Task則進入Sleep。這裏涉及到一些細節,首先看到Worker隊列有很多種,然後線程也不是簡單的沒Task就進入Sleep,而是有更多狀態切換,以達到更好性能。

先忽略Task隊列的細節,因為這涉及到無鎖隊列的實現,認為從一個邏輯上的隊列裏取Task,進入TryExecuteTaskFrom函數。最終進入ExecuteTask函數,執行用户提供的Task,返回值AnyExecuted表示是否執行了Task。

Task處理完後不直接用WaitEvent進入Wait,TaskGraph裏增加了一個Drowsing(休眠)狀態,總共有三個狀態,狀態通過FSleepEvent結構體維護,轉換邏輯在TrySleeping函數。

Running:正在執行Task。

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

Sleeping:一段時間的Drowsing狀態內沒有執行新的Task,調用FEvent.Wait,線程進入阻塞狀態。只有通過WakeUpWorker函數執行FEvent.Trigger後才能恢復執行,同時會把FSleepEvent從SleepEventStack中彈出,把狀態改回Running。

狀態轉換圖如下:

3. Task優先級
遊戲運行過程中會產生大量Task,UE支持為Task指定多個優先級,提供更細粒度的控制,雖然在Async函數裏只提供了一種優先級。這裏只討論Task在WorkerThread中執行的情況,GameThread和RenderThread執行Task另外再討論。

Task優先級定義如下:

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

但用户不能直接指定Task的優先級。用户自定義Task可以通過GetDesiredThread函數指定希望執行的線程、線程優先級、以及Background Task的優先級,最終會設置在TGraphTask的ThreadToExecuteOn屬性上。

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

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

  • ThreadId部分8位
    標識線程的ID,NamedThread下標從0開始,StatsThread=0,RHIThread=1,AudioThread=2,GameThread=3,AnyThread=0xff。
  • QueueIndex部分1位
    MainQueue=1,LocalQueue=2。
  • ThreadPriority部分2位
    指定不同線程優先級,也可以認為是Task的粗粒度優先級,NormalThreadPriority=0,HighThreadPriority=1,BackgroundThreadPriority=2。
  • TaskPriority部分1位
    用户定義的Task細粒度優先級,僅對ThreadPriority=BackgroundThreadPriority時有效,把BackgroundThreadPriority再細分,NormalTaskPriority=0,HighTaskPriority=1。

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

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

以AnyBackgroundThreadNormalTask為例,該Task會在WorkerThread中執行,線程TaskPriority是BackgroundNormal,用户定義TaskPriority是NormalTaskPriority。

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

  • GetThreadIndex
  • GetQueueIndex
  • GetTaskPriority
  • GetThreadPriorityIndex

最終的TaskPriority和WorkerThread種類由ThreadPriority和用用户定義TaskPriority共同決定,代碼在FTaskGraphCompatibilityImplementation::QueueTask中,整理的對應關係如下:

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

以OverflowQueues為例,添加Task代碼如下:

取Task代碼如下,優先級從高到低遍歷:

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

四、TaskGraph管理NamedThread

TaskGraph不僅可以創建WorkerThread執行任務,還能把GameThread、RenderThread等專用線程也納入管理,分派任務給線程執行。

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

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

1. FWorkerThread
表示一個線程,包含相關信息,目前實現只用於NamedThread。

成員

  • FTaskThreadBase*TaskGraphWorker:真正的TaskGraphWorker。
  • bool bAttached:NameThread是否被註冊到TaskGraph系統。

2. FTaskThreadBase
用於讓NamedThread有執行GraphTask的能力。

成員

  • ENamedThreads::Type ThreadId:線程ID。
  • Uint32 PerThreadIDTLSSlot:FWorkerThread對象指針會被存儲到這個Slot對應的TLS中,這樣NamedThread就能取到它了。
  • TArray<FBaseGraphTask*>NewTasks:這個線程要執行的Task。
  • FWorkerThread*OwnerWorker:所有者FWorkerThread的指針。

函數

  • ProcessTasksUntilQuit
  • ProcessTasksUntilIdle:兩個都用於讓NameThreads不斷執行Task,直到線程Idle或者設置RequestQuit標記。
  • EnqueueFromThisThread:向線程添加GraphTask任務,當前執行的線程就是NamedThread。
  • EnqueueFromOtherThread:效果同上,當前執行線程不是NamedThread。
  • Run:內部執行ProcessTasksUntilQuit。

3. FNamedTaskThread
繼承自FTaskThreadBase,用於管理NamedTask。

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

函數
覆寫了ProcessTasksUntilQuit,ProcessTasksUntilIdle,EnqueueFromOtherThread。

4. FThreadTaskQueue
NamedTaskThread擁有的Task隊列。

  • FStallingTaskQueue<FBaseGraphTask, PLATFORM_CACHE_LINE_SIZE, 2> StallQueue:包裝了兩個LockFreelist,對應High和Normal兩個優先級,NamedThread的Task只有這兩個優先級。
  • FEvent*StallRestartEvent:當線程執行完Task後,在該Event上等待

五、創建FWorkerThread對象

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

六、GameThread註冊到TaskGraph

當前線程調用AttachToThread函數可以把自己註冊到TaskGraph中,需要提供一個線程ID。

這是GameThread的註冊方式,在Startup後就立即註冊了:

接着執行到這裏,先根據CurrentThread ID獲取到對應的TaskGraphWorker,然後調用InitializeForCurrentThread,該函數會把OwnerWorker存儲在PerThreadIDTLSSlot的TLS中。

這樣就完成了註冊。

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

七、向NameThread添加Task任務

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

這裏傳入的InThreadToExecuteOn為GameThread,InCurrentThreadIfKnown沒有設置,默認為AnyThread,也可以工作。

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

比較值得注意的GetCurrentThread函數,需要得到當前線程ID,用ENamedThreads表示。

如果是NamedThread,已經設置了TLS,從中取出FWorkerThread指針,然後得到在NamedThreads中的偏移,就是ThreadId。

如果是AnyThread,還會先嚐試獲取當前線程上的ActiveTask,然後獲取ThreadPriority和TaskPriority,一併返回。

最後根據ThreadToExecuteOn和CurrentThreadId,調用EnqueueFromThisThread或EnqueueFromOtherThread,這兩個接口區別為前者是當前線程調用的,後者可以由其他線程調用,也可以由當前線程調用,多了一步線程喚醒操作。

EnqueueFromThisThread把Task加到Queues容器中,QueueIndex決定是MainQueue還是LocalQueue,默認MainQueue,然後從之前的ThreadIdAndIndex裏獲取到TaskPriority,決定加到內部的HighPriority還是NormalPriority Task容器。

EnqueueFromOtherThread也會先把Task加入StallQueue,然後看是否有ThreadToStart,有則調用Trigger,喚醒線程。

八、NamedThread執行Task

以GameThread為例,看如何執行TaskGraph中的Task。

GameThread每幀都會通過World::Tick函數,執行各種Actor的Tick,驅動遊戲世界,而各種Tick函數又通過FTickTaskManager管理,背後再轉換成一個個TGraphTask,放到TaskGraph中執行。

直接進入FTickTaskSequencer::ReleaseTickGroup函數,這裏會執行一個TickGroup中全部的Tick,代碼如下:

然後進入WaitUntilTasksComplete函數,執行這些Task。WaitUntilTasksComplete含義是等待這些Task執行完,方法為創建一個FReturnGraphTask,並把要等待的Task設為前置,FReturnGraphTask作用是把FNamedTaskThread.Queue.QuitForReturn設為true,讓TaskGraph執行完這些Task後就返回。

WaitUntilTasksComplete

之後執行到ProcessTasksUntilQuit和ProcessTasksNamedThread,不斷從Queue中取GraphTask並執行,直到執行了FReturnGraphTask,然後返回。

我們之前通過Async函數向GameThread添加的Task,也是在這裏從Queue中取出,然後被執行的。

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

九、GraphTask的依賴關係

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

GraphTask依賴關係需要解決兩個問題:

  • 如何組織Task,按照依賴順序執行這些Task;
  • 等待依賴的Task執行完成會可能造成線程休眠,如何喚醒線程。

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

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

簡單時序圖如下:

1. GraphEvent
這裏Task依賴通過FGraphEventArray結構實現,而FGraphEventArray其實是一組FGraphEvent的引用,FGraphEvent是Task依賴的關鍵。

GraphEvent可以理解為GraphTask相關的“事件”,GraphTask之間通過“事件”聯繫。

2. FGraphEvent
包含了一系列後置Task,該GraphEvent是它們的觸發條件。

成員

  • TClosableLockFreePointerListUnorderedSingleConsumer<FBaseGraphTask, 0> SubsequentList:後置Task,是無鎖鏈表。
  • FGraphEventArray EventsToWaitFor:該GraphEvent要等待的其他GraphEvent數組,其實只有一個元素,其他GraphEvent完成後,該EventGraph才會觸發,在DontCompleteUntil裏設置。

方法

  • AddSubsequent:添加一個後置Task。
  • DontCompleteUntil:提供一個前置GraphEvent,前置完成後自己才觸發。

回顧一下TGraphTask的成員:

  • Subsequents:該GraphTask對應的FGraphEvent。
  • NumberOfPrerequistitesOutstanding:該GraphTask有多少個前置待執行。
  • ConstructAndDispatchWhenReady函數會返回GraphTask對應的GraphEvent,外部就能操作它了。

3. CreateTask
CreateTask方法可以接受Prerequistes參數,得到該GraphTask的前置,接着進入TGraphTask::SetupPrereqs函數。

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

4. DispatchSubsequents
在TGraphTask執行完後,會通過Subsequents對象執行DispatchSubsequents,讓其他依賴自己的Task執行。這裏要分有無EventsToWaitFor的情況。

無EventsToWaitFor:
TGraphTask執行完後,就立即觸發完成事件,需要遍歷所有SubsequentList裏的後置Task,調用ConditionalQueueTask,如果後置的所有前置都已被觸發,就調用QueueTask,把自己加入Task隊列,等待執行。

有EventsToWaitFor:
有時候TGraphTask自己完成了,但不想立即觸發事件,還想等待另一個GraphEvent完成後再觸發,

比如多線程動畫更新裏的TickFunction函數,對應的事件要等到TickCompletionEvent完成後再觸發。相當於TickFunction Task已經在執行了,但還想給它添加前置一樣。

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

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

這只是簡單的TaskGraph依賴關係,當然可以自己組合出一些多前置,多後置的TaskGraph依賴,背後原理是一樣的。

十、NamedThread Sleep/喚醒

多線程動畫例子中,如果FParallelAnimationEvaluationTask執行時間過長,GameThread已經把PrePhysics階段的所有Tick都執行完了,就會進入Sleep狀態,等FParallelAnimationEvaluationTask執行完後再喚醒GameThread繼續執行。

1. 進入Sleep
GameThread在Tick時會執行ProcessTasksNamedThread,While循環從Queue中獲取下一個Task,執行到ReturnTask之前都不會退出,如果取不到Task了,説明需要等其他線程執行完前置Task,那麼GameThrad自身會在這個Queue的StallRestartEvent上Wait,進入Sleep狀態。

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

看下StallQueue的Pop函數:

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

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

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

2. 喚醒
當調用EnqueueFromOtherThread添加Task後,會判斷線程是否在Sleep狀態,然後執行StallRestartEvent.Trigger()喚醒線程,繼續執行。

StallQueue的Push函數如下:

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

十一、一些Task同步函數

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

1. TaskGraph接口

  • WaitUntilTasksComplete(Tasks)
    等待多個GraphEvent執行完,內部做法是增加一個FReturnTask,把傳入的Tasks作為其前置,然後調用ProcessThreadUntilRequestReturn。

比如如下代碼:

  • ProcessThreadUntilIdle
    在NamedThread上調用,阻塞執行當前Queue裏的所有Task,直到完成。
  • ProcessThreadUntilRequestReturn
    與ProcessThreadUntilIdle類似,只是需要預先添加一個ReturnTask任務。

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

2. GraphEvent接口

  • DontCompleteUntil
    GraphEvent的函數,之前動畫藍圖例子已介紹過,會給當前GraphEvent設置另一個Event作為EventsToWaitFor,在EventsToWaitFor觸發後,才觸發當前的GraphEvent。

流程圖見上面。

  • Wait
    內部調用了TaskGraph的WaitUntilTasksComplete接口,把自己作為參數傳入,效果與WaitUntilTasksComplete相同。

這是侑虎科技第1932篇文章,感謝作者南京周潤發供稿。歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ羣:793972859)

作者主頁:https://www.zhihu.com/people/xu-chen-71-65

再次感謝南京周潤發的分享,如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ羣:793972859)

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.