UE5多线程|TaskGraph
- 作者:admin
- /
- 时间:4小时前
- /
- 浏览:19 次
- /
- 分类:厚积薄发
【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
NamedThreads:管理了所有NamedThread。
FTaskGraphImplementation:旧TaskGraph子类实现,不看了。
2. FScheduler
FScheduler用于创建、管理Workder线程,以及把Task分派给Worker线程。
成员
- TArray<TUniquePtr
> WorkerThreads:工作线程。 - TAlignedArray<FSchedulerTls::FLocalQueueType> 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<class FThreadImpl, ESPMode::ThreadSafe> Impl:实际的Frunnable。
4. FThreadImpl
FThread的具体实现,继承自Frunnable。
方法
Run:调用了成员ThreadFunction。
成员
- TUniqueFunction<void()> ThreadFunction:线程要执行的函数,就是WorkerMain。
- TUniquePtr
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

四、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)

