UE4中的TaskGraph用于实现将将多个Taskes放入多个线程执行,并且可以设定这些Task之间的依赖,UE4中很多地方的多线程操作都通过该系统实现。
TaskGraph系统所维护的线程,与游戏线程,渲染线程是独立的,TaskGraph系统中会维护一套内部的线程池,有与游戏线程或渲染线程相对应的独立线程,来执行相应指令。结构如下。
FTaskGraphImplementation为TaskGraph系统顶层管理类,为单例结构,在LauchEngineLoop类的PreInit中创建全局单例:
FTaskGraphInterface::Startup( FPlatformMisc::NumberOfCores() );
其中创建WorkerThreads任务线程池以供使用。
WorkerThreads[ThreadIndex].RunnableThread = FRunnableThread::Create(&Thread(ThreadIndex), *Name, StackSize, ThreadPri, Affinity);
对于引擎内几个重要的线程,并不需要在TaskGraph中生成实际RunableThread,而是直接将对应线程注册至TaskGraph系统中(属于FNamedTaskThread),如游戏线程,TaskGraph里的任务直接在主线程执行即可。
FTaskGraphInterface::Get().AttachToThread( ENamedThreads::GameThread );
每个任务线程里执行相应的任务,不同线程间的任务可以存在依赖关系。
TaskGraph系统执行机制如下。
TGraphTask的CreateTask方法创建一个任务,TTask是传进来的模板类型typename TTask
static FConstructor CreateTask(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
int32 NumPrereq = Prerequisites ? Prerequisites->Num() : 0;
if (sizeof(TGraphTask) <= FBaseGraphTask::SMALL_TASK_SIZE)
{
void *Mem = FBaseGraphTask::GetSmallTaskAllocator().Allocate();
return FConstructor(new (Mem) TGraphTask(TTask::GetSubsequentsMode() == ESubsequentsMode::FireAndForget ? NULL : FGraphEvent::CreateGraphEvent(), NumPrereq), Prerequisites, CurrentThreadIfKnown);
}
return FConstructor(new TGraphTask(TTask::GetSubsequentsMode() == ESubsequentsMode::FireAndForget ? NULL : FGraphEvent::CreateGraphEvent(), NumPrereq), Prerequisites, CurrentThreadIfKnown);
}
参数CurrentThreadIfKnown指定当前线程,决定创建的任务最终是放入当前线程还是其它线程。
TGraphTask的ConstructAndDispatchWhenReady方法构建并分发。
template<typename...T>
FGraphEventRef ConstructAndDispatchWhenReady(T&&... Args)
{
new ((void *)&Owner->TaskStorage) TTask(Forward<T>(Args)...);
return Owner->Setup(Prerequisites, CurrentThreadIfKnown);
}
TaskStorage指向创建该任务的对象,该对象需要包含GetDesiredThread方法与GetSubsequentsMode方法。
**GetDesiredThread方法**:指定任务所在线程。
**GetSubsequentsMode方法**:指定依赖模式,TrackSubsequents表示需要依赖关系,FireAndForget表示不需要依赖其它任务,直接放入队列。
例如对于渲染线程的指令,都派生于FRenderCommand,可以看到FRenderCommand定义中指定了这两个方法:
/** The parent class of commands stored in the rendering command queue. */
class RENDERCORE_API FRenderCommand
{
public:
// All render commands run on the render thread
static ENamedThreads::Type GetDesiredThread()
{
check(!GIsThreadedRendering || ENamedThreads::RenderThread != ENamedThreads::GameThread);
return ENamedThreads::RenderThread;
}
static ESubsequentsMode::Type GetSubsequentsMode()
{
// Don't support tasks having dependencies on us, reduces task graph overhead tracking and dealing with subsequents
return ESubsequentsMode::FireAndForget;
}
};
TGraphTask的成员Subsequents指向当前任务的后续事件列表(GetCompletionEvent方法获取该引用),即指向图中的EventX与EventY的指针,事件内部存储一个等待执行的任务列表SubsequentList。仅当GetSubsequentsMode方法返回TrackSubsequents时该Subsequents才有效,否则为空。Subsequents为FGraphEvent类型,调用DispatchSubsequents来执行后续事件,即行完成时说明该事件已经完成:
bool IsComplete() const
{
return SubsequentList.IsClosed();
}
ConstructAndDispatchWhenReady返回的即为TGraphTask的成员Subsequents,创建的地方可以持有该事件引用,来判断事件是否完成,游戏线程与渲染线程同步取通过该方式保证一帧内的渲染命令都执行完成,再继续游戏线程。
任务构建流程如下:
FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
FTaskGraphInterface::Get().ProcessThreadUntilRequestReturn(ENamedThreads::RenderThread);
ProcessTasksUntilIdle是执行任务队列,直到线程为Idle状态或调用了RequestQuit,没有主动RequestQuit的情况下,执行完了任务队列也会直接返回。
ProcessThreadUntilRequestReturn同样是执行任务队列,执行完任务队列,也必须等待RequestQuit才会返回
FReturnGraphTask任务用于发送RequestQuit任务,该任务的DoTask中执行指定线程的返回请求:
FTaskGraphInterface::Get().RequestReturn(ThreadToReturnFrom);
备注:渲染线程会一直调用该函数来处理任务队列。
FTaskGraphInterface::Get().ProcessThreadUntilRequestReturn(ENamedThreads::RenderThread)
Tick底层执行流程如下:
关键点:
UWorld中调用最顶层Tick;
FTickTaskManager做顶层管理,全局单例;
FTickTaskLevel维护一个AllEnabledTickFunctions列表,每个元素为TickFunction及其先决依赖条件(同一个Group保证时序);
每次Tick,首先会注册两个物理TickFunction到AllEnabledTickFunctions列表中,分别为StartPhysicsTickFunction与EndPhysicsTickFunction,用于划分分组阶段;
FTickFunctionTask定义TickFunction对应任务,其Context成员存储Task所在的线程名;
FTickTaskSequencer为每个TickFunction生成TickTask,并按分组存储至TickTasks列表中,优化级高的会存储至HiPriTickTasks;
TaskGraph系统(全局单例)用于实现将将多个Taskes放入多个线程执行,并且可以设定这些Task之间的依赖,通过该TaskGraph来完成依赖判断及执行。如上图中,EventX和EventY可以理解为TaskC的先决条件,需要两个条件完成后再可以执行TaskC,可见,该TaskGraph非常适合用于Tick管理当中,处理依赖及执行;
按各物理阶段进行分组,每个分组任务之间是串行的,必须在执行上一阶段分组Tick完成之后(否则阻塞)才能执行下一阶段的分组Tick任务;
最终在Task中执行ExecuteTask并最终执行Actor或Component的Tick函数。
提示::
每个TickFunction都可以指定TickGroup,但是这个TickGroup并不代表最终实际执行的TickGroup。根据每个Tick注册的Prerequisite不同,Tick的执行Group会被延迟。例如,如果TickFunction要求的Prerequisite是在PostPhiscs中的,那么即便它自己是注册为PrePhyiscs,也会被推迟到Post才能执行, TickFunction的ActualStartTickGroup会变成实际的Tick执行组。
详细类调用关系图如下:
[1] Unreal - Tick Functions, Delta Time, and the Task Grap