![]()
【USparkle專欄】如果你深懷絕技,愛“搞點研究”,樂于分享也博采眾長,我們期待你的加入,讓智慧的火花碰撞交織,讓知識的傳遞生生不息!
這是侑虎科技第1932篇文章,感謝作者南京周潤發供稿。歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯系我們,一起探討。(QQ群:793972859)
作者主頁:
https://www.zhihu.com/people/xu-chen-71-65
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 NamedThreads:管理了所有NamedThread。
FTaskGraphImplementation:舊TaskGraph子類實現,不看了。
2. FScheduler
FScheduler用于創建、管理Workder線程,以及把Task分派給Worker線程。
成員
TArray > WorkerThreads:工作線程。
TAlignedArray WorkerLocalQueues:WorkerThread對應的Task。
TAlignedArray 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 Impl:實際的Frunnable。
4. FThreadImpl
FThread的具體實現,繼承自Frunnable。
方法
Run:調用了成員ThreadFunction。
成員
TUniqueFunction ThreadFunction:線程要執行的函數,就是WorkerMain。
TUniquePtr RunnableThread:對應的FRunnableThread對象。
5. TGraphTask
TaskGraph系統中管理的Task,不直接調用用戶提供的Task函數,而是把函數封裝成一個user defined task,存儲在其中。
成員
TAlignedBytes TaskStorage:存儲的user defined task,類型由模板指定。
FGraphEventRef Subsequents:存儲哪些GraphTask以我們為前置。
方法
CreateTask:創建一個新GraphTask。
ExecuteTask:執行Task。
SetupPrereqs:設置Task前置。
6. TAsyncGraphTask
屬于user defined task,是UE為實現Async函數而創建的類。
成員
TUniqueFunction 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 ::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 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 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 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相同。
文末,再次感謝南京周潤發 的分享, 作者主頁:https://www.zhihu.com/people/xu-chen-71-65, 如果您有任何獨到的見解或者發現也歡迎聯系我們,一起探討。(QQ群: 793972859 )。
![]()
近期精彩回顧
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
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.