网易首页 > 网易号 > 正文 申请入驻

UE5多线程|TaskGraph

0
分享至


【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.

相关推荐
热点推荐
云南省德宏州委副书记、州长卫岗接受审查调查

云南省德宏州委副书记、州长卫岗接受审查调查

界面新闻
2026-01-03 14:40:33
当石油被抢后,西方普遍认为北京只能认栽,怎料中方一招逆转局面

当石油被抢后,西方普遍认为北京只能认栽,怎料中方一招逆转局面

小lu侃侃而谈
2026-01-02 19:54:47
宝马突然大范围调价,最高降30万,宝马中国回应→

宝马突然大范围调价,最高降30万,宝马中国回应→

第一财经资讯
2026-01-02 21:33:23
女网红自称与鹿晗林更新等多名男艺人存私密关系,多方发声辟谣并称将追责,律师分析

女网红自称与鹿晗林更新等多名男艺人存私密关系,多方发声辟谣并称将追责,律师分析

潇湘晨报
2026-01-03 14:16:19
后背发凉!一月入3万36岁女高管,失业8个月加离婚,如今送外卖了

后背发凉!一月入3万36岁女高管,失业8个月加离婚,如今送外卖了

火山詩话
2026-01-02 19:14:41
“最美新生儿”火了,似乎在娘胎整容化妆一般,全网都想沾喜气

“最美新生儿”火了,似乎在娘胎整容化妆一般,全网都想沾喜气

菁妈育儿
2026-01-03 12:44:30
各大卫视跨年晚会过后,终于意识到内娱真的完了

各大卫视跨年晚会过后,终于意识到内娱真的完了

星宿影视鸭
2026-01-02 15:11:22
《人民日报》:真正能给你撑腰的,是足够的金钱,稳定的情绪....

《人民日报》:真正能给你撑腰的,是足够的金钱,稳定的情绪....

诗词中国
2026-01-02 20:36:32
丢掉幻想 美西方见不得一个统一强大的中国

丢掉幻想 美西方见不得一个统一强大的中国

看看新闻Knews
2026-01-02 23:02:35
师长回乡省亲,途中与车夫一路攀谈,到家门口时才发现这位车夫就是自己的父亲

师长回乡省亲,途中与车夫一路攀谈,到家门口时才发现这位车夫就是自己的父亲

史海孤雁
2026-01-02 17:57:19
承诺“每人送台车”的老板,清空账号!后续来了

承诺“每人送台车”的老板,清空账号!后续来了

南方都市报
2026-01-03 09:19:52
震惊!济宁一加油站充5000送5000,老板跑路,网友称活动搞好几年

震惊!济宁一加油站充5000送5000,老板跑路,网友称活动搞好几年

火山詩话
2026-01-02 16:45:48
司晓迪事件最全瓜!檀健次私下是舔狗,林更新爱约,张一山最大方

司晓迪事件最全瓜!檀健次私下是舔狗,林更新爱约,张一山最大方

萌神木木
2026-01-03 11:40:35
网友偶遇李湘母女,现实中看其实也就是微胖身材

网友偶遇李湘母女,现实中看其实也就是微胖身材

娱乐顺风车666
2026-01-03 10:43:09
广州小学生被抽血事件:告诉爸妈就扣小红花

广州小学生被抽血事件:告诉爸妈就扣小红花

每日一见
2026-01-02 12:21:28
大兴机场被北漂睡成“洗浴中心”,撕开当下社会最体面的一幕

大兴机场被北漂睡成“洗浴中心”,撕开当下社会最体面的一幕

毒sir财经
2026-01-02 22:03:01
同胞遇害抛尸柬埔寨,六名中国男子落网,还听媒体鼓动去旅游吗?

同胞遇害抛尸柬埔寨,六名中国男子落网,还听媒体鼓动去旅游吗?

你食不食油饼
2026-01-02 20:47:35
“最快女护士”张水华宣布辞职,丈夫发声!她元旦刚拿下马拉松比赛冠军,曾50天内赢得20万元奖金

“最快女护士”张水华宣布辞职,丈夫发声!她元旦刚拿下马拉松比赛冠军,曾50天内赢得20万元奖金

每日经济新闻
2026-01-02 23:20:51
“玻璃发生撞击,已经裂透”,人命关天!央视披露:从发现裂纹到任务成功,我国首次应急发射都经历了什么?

“玻璃发生撞击,已经裂透”,人命关天!央视披露:从发现裂纹到任务成功,我国首次应急发射都经历了什么?

极目新闻
2026-01-03 08:28:41
周总理提议让叶帅当外长,毛主席赞成,叶帅摇摇头:更想待在部队

周总理提议让叶帅当外长,毛主席赞成,叶帅摇摇头:更想待在部队

历史龙元阁
2026-01-02 13:35:07
2026-01-03 15:12:49
侑虎科技UWA incentive-icons
侑虎科技UWA
游戏/VR性能优化平台
1536文章数 986关注度
往期回顾 全部

科技要闻

比亚迪销冠!特斯拉2025年交付量跌逾8%

头条要闻

蔡正元被判刑3年半 柯文哲致电哽咽:都是因为我

头条要闻

蔡正元被判刑3年半 柯文哲致电哽咽:都是因为我

体育要闻

快船似乎又行了

娱乐要闻

“国服嫂子”司晓迪,曝与多位男星私照

财经要闻

人工智能四问:投资泡沫出现了吗?

汽车要闻

奕派科技全年销量275,752辆 同比增长28.3

态度原创

时尚
艺术
数码
手机
公开课

睡衣穿对了,连躺平都带着高级感

艺术要闻

15幅 苏联时期静物与花卉油画

数码要闻

曝苹果春季推平价版MacBook:配12.9英寸屏,售价599 - 899美元

手机要闻

用户发现苹果iPhone手机电池会在飞行途中鼓包,落地后恢复正常

公开课

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

无障碍浏览 进入关怀版