UE4 多线程源码浅析(3——TaskGraph)

这篇具有很好参考价值的文章主要介绍了UE4 多线程源码浅析(3——TaskGraph)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文章只是我个人在学习虚幻引擎过程中的一些理解,不一定正确,若有说的不对的地方,欢迎指正。

前两篇我们分别讲了虚幻多线程的基础线程系统(FRunnable)和异步任务系统(AsyncTask),本篇我们来讲讲虚幻多线程的最后一员大将——TaskGraph
TaskGraph字面意思是任务图,可以把它看成是超进化版的线程池,相对的它的源码也变得更加复杂。 TaskGraph实现了任务之间额等待机制,因此它和前两种多线程系统最大的区别在于它可以表示出任务与任务之间相互依赖的关系。
例如此刻我们有五个任务(A~E),各个任务彼此之间存在相互依赖的关系表示如下:
UE4 多线程源码浅析(3——TaskGraph),虚幻引擎,ue4,算法,游戏引擎,虚幻,游戏程序

被依赖的任务指向依赖的任务(例如B和C依赖于A,即A必须比B、C先完成)
TaskGraph就可以通过自身机制来实现这种关系。
TaskGraph有三个主要组成部分:任务图抽象类(FTaskGraphImplementation、FTaskGraphInterface)、任务图中的线程抽象类(FWorkerThread)和任务抽象类(TGraphTask、FBaseGraphTask……)。

一.任务图抽象类:

1.FTaskGraphInterface

FTaskGraphInterfaceTaskGraph系统主体的基类或者说是核心层,提供了一些系统需要的接口,但是还没有实现。源码在TaskGraphInterfaces.h文件中,具体路径在下方,FTaskGraphInterface大致源码如下:

class FTaskGraphInterface
{
	friend class FBaseGraphTask;

	virtual void QueueTask(class FBaseGraphTask* Task, ENamedThreads::Type ThreadToExecuteOn, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread) = 0;

public:

	//……

	static CORE_API void Startup(int32 NumThreads);
	static CORE_API void Shutdown();
	static CORE_API bool IsRunning();
	static CORE_API FTaskGraphInterface& Get();
	virtual ENamedThreads::Type GetCurrentThreadIfKnown(bool bLocalQueue = false) = 0;
	virtual	int32 GetNumWorkerThreads() = 0;

	//……

	virtual void AttachToThread(ENamedThreads::Type CurrentThread) = 0;

	//……

	virtual void WaitUntilTasksComplete(const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread) = 0;
	virtual void TriggerEventWhenTasksComplete(FEvent* InEvent, const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread, ENamedThreads::Type TriggerThread = ENamedThreads::AnyHiPriThreadHiPriTask) = 0;
	void WaitUntilTaskCompletes(const FGraphEventRef& Task, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
	{
		//……
	}
	void TriggerEventWhenTaskCompletes(FEvent* InEvent, const FGraphEventRef& Task, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread, ENamedThreads::Type TriggerThread = ENamedThreads::AnyHiPriThreadHiPriTask)
	{
		//……
	}

	//……
};

FTaskGraphInterface提供接口如下:
QueueTask——根据线程把任务放入到对应的队列中,函数接收三个参数,任务实例(Task)、执行任务的线程(ThreadToExecuteOn)、当前线程(CurrentThreadIfKnown);
Startup——函数初始化TaskGraph系统;
Shutdown——当TaskGraph系统处于空闲状态时,函数关闭系统,否则不起作用;
IsRunning——判断TaskGraph系统是否在运行;
Get——获取TaskGraph系统单例;
GetCurrentThreadIfKnown——获取当前线程;
GetNumWorkerThreads——获取工作线程数;
AttachToThread——把指定线程加入TaskGraph
WaitUntilTasksComplete——执行多个异步任务,然后等待这些任务完成,接收两个参数,异步任务列表(Tasks)、当前线程(CurrentThreadIfKnown),必须是一个命名线程;
TriggerEventWhenTasksComplete——执行多个异步任务,完成时触发一个事件,接收三个参数,触发的事件(InEvent)、异步任务列表(Tasks)、当前线程(CurrentThreadIfKnown);
WaitUntilTaskCompletes——执行单个异步任务,然后等待该任务完成,接收两个参数,异步任务(Task)、当前线程(CurrentThreadIfKnown),必须是一个命名线程;
TriggerEventWhenTaskCompletes——执行单个异步任务,完成时触发一个事件,接收三个参数,触发的事件(InEvent)、异步任务(Task)、当前线程(CurrentThreadIfKnown)。

注:Engine\Source\Runtime\Core\Public\Async\TaskGraphInterfaces.h

2.FTaskGraphImplementation:

FTaskGraphImplementationFTaskGraphInterface的功能实现类/表现层,它实现了核心层提供的接口和一些必要的属性,我们只列举几个FTaskGraphInterface没有的接口和属性,大致源码如下:

class FTaskGraphImplementation : public FTaskGraphInterface
{
public:

	//……

	void StartTaskThread(int32 Priority, int32 IndexToStart)
	{
		//……
	}

	void StartAllTaskThreads(bool bDoBackgroundThreads)
	{
		//……
	}

	FBaseGraphTask* FindWork(ENamedThreads::Type ThreadInNeed)
	{
		//……
	}

	//……

	void SetTaskThreadPriorities(EThreadPriority Pri)
	{
		//……
	}

private:

	//……

	FWorkerThread			WorkerThreads[MAX_THREADS];
	
	int32				NumThreads;

	int32				NumNamedThreads;

	int32				NumTaskThreadSets;

	int32				NumTaskThreadsPerSet;

	bool				bCreatedHiPriorityThreads;
	bool				bCreatedBackgroundPriorityThreads;
	
	//……
};

接口介绍:
StartTaskThread——根据传入的优先级(Priority)和线程索引(IndexToStart)启动一个线程用来执行任务;
StartAllTaskThreads——启动所有线程执行任务;
FindWork——获取一个任务;
SetTaskThreadPriorities——顾名思义,设置线程的优先级;
属性介绍:
WorkerThreads[MAX_THREADS]——类型为FWorkerThread,是工作线程和数据的封装;
NumThreads——类型为32位整数,表示真正使用的线程数;
NumNamedThreads——类型为32位整数,表示NameThread线程的数量;
NumTaskThreadSets——类型为32位整数,表示线程集的数量;
NumTaskThreadsPerSet——类型为32位整数,表示每个线程集的线程数量,至少1个,最多3个;
bCreatedHiPriorityThreads——类型为布尔值,是一个标志,表示是否创建高先级线程;
bCreatedBackgroundPriorityThreads——类型为布尔值,是一个标志,表示是否创建低优先级线程;

二.线程抽象类:

我们上一篇讲过异步任务机制中,线程池的线程是基于FRunnable机制实现的,那么对于本篇的TaskGraph呢?它的线程又是怎么实现的?事实上,虚幻引擎的所有多线程机制都和FRunnable有千丝万缕的关系,这个小节我们将从TaskGraph的线程抽象类(FWorkerThread)出发,介绍TaskGraph是如何用FRunnable实现的。

1.FWorkerThread:

FWorkerThread代表TaskGraph中的一个线程,它是一个辅助结构体,内部聚合了几个各线程需要的属性,源码很简单:

struct FWorkerThread
{
	FTaskThreadBase*	TaskGraphWorker;
	FRunnableThread*	RunnableThread;
	bool			bAttached;

	FWorkerThread()
		: TaskGraphWorker(nullptr)
		, RunnableThread(nullptr)
		, bAttached(false)
	{
	}
};

TaskGraphWorker——类型为FTaskThreadBase,是真正的线程,后面会仔细介绍;
RunnableThread——类型是我们熟悉的FRunnableThread,不知道的小伙伴可以去看**《UE4 多线程源码浅析(1——FRunnable)》,这里不再多说;
bAttached——类型为布尔值,是一个标志,表示是否注册到
TaskGraph**系统中;

2.FTaskThreadBase:

FTaskThreadBase是真正的线程的封装类,是用来管理真正的线程的基类(FTaskThreadBase有两个子类,分别对应FNamedTaskThreadFTaskThreadAnyThread的实现)。继承自FRunnableFSingleThreadRunnable。这个类实现了FRunnable的API,我们不直接使用这个类,因为这些线程是由虚幻引擎在启动时初始化的。
FSingleThreadRunnable只是一个接口类,只有一个析构函数和Tick。

class CORE_API FSingleThreadRunnable
{
public:
	virtual ~FSingleThreadRunnable() { }
	virtual void Tick() = 0;
};

class FTaskThreadBase : public FRunnable, FSingleThreadRunnable
{
public:

	//……

	virtual void ProcessTasksUntilQuit(int32 QueueIndex) = 0;

	//……

	virtual void EnqueueFromThisThread(int32 QueueIndex, FBaseGraphTask * Task)
	{
		//……
	}

	virtual void RequestQuit(int32 QueueIndex) = 0;

	virtual bool EnqueueFromOtherThread(int32 QueueIndex, FBaseGraphTask * Task)
	{
		//……
	}

	virtual void WakeUp()
	{
		//……
	}
	
	//……

	//……(FRunnable API)

protected:

	ENamedThreads::Type	ThreadId;
	uint32			PerThreadIDTLSSlot;
	FThreadSafeCounter	IsStalled;
	TArray<FBaseGraphTask*> NewTasks;
	FWorkerThread* 		OwnerWorker;
};

接口介绍:
ProcessTasksUntilQuit——用于NameThread开始执行任务并在完成任务后返回;
EnqueueFromThisThread——当想要执行的线程与某正在工作的线程相同时,把任务添加到该线程的任务等待队列,如果该线程是NameThread直接添加到NameThread的私有队列,接收两个参数:队列的索引(QueueIndex)和要添加的任务(Task);
RequestQuit——线程空闲时调用,接收一个参数:队列的索引(QueueIndex);
EnqueueFromOtherThread——当想要执行的线程与某正在工作的线程不同时,把任务添加到想要执行的线程的任务等待队列,接收两个参数:队列的索引(QueueIndex)和要添加的任务(Task);
WakeUp——唤醒线程;
属性介绍:
ThreadId——类型为ENamedThreads::Type,表示NameThread线程的类型;
PerThreadIDTLSSlot—— 类型为无符号32位整数,表示存储FTaskThread指针的TLS槽位;
IsStalled——类型为
FThreadSafeCounter*,表示线程安全计数器;
NewTasks——类型为TArray<FBaseGraphTask*>,表示任务等待队列;
OwnerWorker——类型为FWorkerThread*,表示指向所属的FWorkerThread

a.FNamedTaskThread:

FNamedTaskThread是用来管理命名线程(NamedThread)的类,大致源码如下:

class FNamedTaskThread : public FTaskThreadBase
{
public:

	//……

	uint64 ProcessTasksNamedThread(int32 QueueIndex, bool bAllowStall)
	{
		//……
	}
	
	//……

	/** 任务队列结构体定义 **/
	struct FThreadTaskQueue
	{
		/*队列数据结构*/
		FStallingTaskQueue<FBaseGraphTask, PLATFORM_CACHE_LINE_SIZE, 2> StallQueue;

		/** 我们需要禁止处理循环的重新进入 **/
		uint32 RecursionGuard;

		/** 执行返回任务的时候让我跳出循环 **/
		bool QuitForReturn;

		/** 执行返回任务的时候让我跳出循环 **/
		bool QuitForShutdown;

		/** 当我们跳出任务的时候阻塞的事件 **/
		FEvent* StallRestartEvent;

		//……
	};


	FORCEINLINE FThreadTaskQueue& Queue(int32 QueueIndex)
	{
		//……
	}

	FORCEINLINE const FThreadTaskQueue& Queue(int32 QueueIndex) const
	{
		//……
	}

	FThreadTaskQueue Queues[ENamedThreads::NumQueues];
};

省略部分东西,剩下的不多,属性就一个任务队列,队列的类型(FThreadTaskQueue)定义在类内,接口如下:
ProcessTasksNamedThread——执行该NameThread线程,队列的索引(QueueIndex)、是否允许延迟启动(bAllowStall);
Queue——作用都是从任务队列中获取指定的任务,有两个重定义,返回值一个是可修改一个是不可修改;

b.FTaskThreadAnyThread:

FTaskThreadAnyThread是用来管理任意线程(AnyThread)的类,大致源码如下:

class FTaskThreadAnyThread : public FTaskThreadBase
{
public:
	
	//……

	virtual bool IsProcessingTasks(int32 QueueIndex) override
	{
		//……
	}

	//……

	uint64 ProcessTasks()
	{
		//……
	}

	/** 任务队列结构体 **/
	struct FThreadTaskQueue
	{
		/** 当我们跳出任务的时候阻塞的事件 **/
		FEvent* StallRestartEvent;

		/** 我们需要禁止处理循环的重新进入 **/
		uint32 RecursionGuard;

		/** 执行返回任务的时候让我跳出循环 **/
		bool QuitForShutdown;

		/** 是否延迟调优 **/
		bool bStallForTuning;

		/** 延迟调优的作用域锁 **/
		FCriticalSection StallForTuning;

		//……
	};

	FBaseGraphTask* FindWork();

	/** 是一个队列的数组,只有数组的第一个队列适用于任意线程 **/
	FThreadTaskQueue Queue;

	/* 从翻译来看是个优先级索引 */
	int32 PriorityIndex;
};

类内部定义了一个任务队列结构体(FThreadTaskQueue
接口介绍:
IsProcessingTasks——线程是否正在执行任务,接受一个参数:队列的索引(QueueIndex);
ProcessTasks——执行任务知直到空闲,若bAllowStall为真可能会被阻塞,接收两个参数:队列的索引(QueueIndex)、如果为true,则当线程运行完所有任务时,线程将在stall事件上阻塞(bAllowStall);
FindWork——返回一个未执行的任务;
属性介绍:
Queue——类型是FThreadTaskQueue,是一个队列的数组,只有数组的第一个队列适用于任意线程;
PriorityIndex——类型是32位整数,从翻译来看是个优先级索引;

三.任务抽象类:

前面我们介绍了一下TaskGraph系统的主体类和线程抽象类,我们似乎没看到什么有关任务依赖关系的代码。事实上,任务依赖功能是在任务抽象类(Task)中实现的。对于任务抽象类中,我们将从TGraphTask出发了解任务源码的基础架构,了解一下它系统中各个任务的组织方式。

1.TGraphTask:

在**《UE4 多线程的使用》中我们知道用TGraphTask**启动执行我们自定义的任务,其实就是把一个用户自定义的任务作为模板参数嵌入,用于完成工作。同时,这个类提供了设置和处理先决任务和后续条任务的功能。

a.FConstructor:

在了解TGraphTask之前我们先了解一下辅助类FConstructor,它是一个工厂类,构造我们的任务,为任务的执行做准备,实现在TGraphTask内部大致源码如下:

class FConstructor
	{
	public:
		
		template<typename...T>
		FGraphEventRef ConstructAndDispatchWhenReady(T&& ... Args)
		{
			//……
		}

		template<typename...T>
		TGraphTask* ConstructAndHold(T&& ... Args)
		{
			//……
		}

	private:
		friend class TGraphTask;


		TGraphTask* 					Owner;

		const FGraphEventArray* 			Prerequisites;
		
		ENamedThreads::Type				CurrentThreadIfKnown;

		//……
	};

辅助类挺简单的,只是实现了两个构造启动任务的接口和一些必要属性:
接口介绍:
ConstructAndDispatchWhenReady——构造一个任务对象,任务传入需要的参数(例如依赖的任务和希望执行的线程),然后启动执行任务;
ConstructAndHold——构造一个任务对象,任务传入需要的参数(例如依赖的任务和希望执行的线程),然后Hold住,即停止继续执行;
属性介绍:
Owner——类型为一个指向TGraphTask的指针,指向我们构造出来的任务对象;
Prerequisites——类型为指向FGraphEventArray的指针,表示依赖的任务列表;
CurrentThreadIfKnown——类型时一个ENamedThreads::Type,看注释的解释是说,若当前线程已知,该变量就是当前线程的类型,未知就为任意线程;

注:不了解工厂模式的小伙伴可以去查一下资料,游戏引擎大量运用了该模似

b.TGraphTask:

接下来才是任务类的主体——TGraphTask,它继承自FBaseGraphTask,一个TGraphTask对象代表一个任务,系统对任务的调度都是以TGraphTask为最小粒度,我们平时调用的CreateTask就是在这里实现的。源码如下:

template<typename TTask>
class TGraphTask final : public FBaseGraphTask
{
public:

	//……

	static FConstructor CreateTask(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
	{
		//……
	}

	//……

private:

	//……

	void SetupPrereqs(const FGraphEventArray * Prerequisites, ENamedThreads::Type CurrentThreadIfKnown, bool bUnlock)
	{
		//……
	}

	FGraphEventRef Setup(const FGraphEventArray * Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
	{
		//……
	}

	TGraphTask* Hold(const FGraphEventArray * Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
	{
		//……
	}

	static FConstructor CreateTask(FGraphEventRef SubsequentsToAssume, const FGraphEventArray * Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
	{
		//……
	}

	TAlignedBytes<sizeof(TTask), alignof(TTask)> 	TaskStorage;

	bool						TaskConstructed;
	
	FGraphEventRef					Subsequents;
};

接口介绍:
CreateTask——这是CreateTask的一个版本,是一个工厂接口,借助辅助类构造我们的任务对象。接收两个参数:依赖的任务(Prerequisites)和希望执行的线程类型(CurrentThreadIfKnown),返回一个辅助类(FConstructor)对象;
SetupPrereqs——FConstructor会调用该接口,它创建一个完成事件,将自己添加到依赖任务的后续,接受三个参数:依赖的任务(Prerequisites)、希望执行的线程(CurrentThreadIfKnown)和是否锁住(bUnlock);
Setup——FConstructor会调用该接口,它创建一个完成事件,将自己添加到依赖任务的后续,接受两个参数:依赖的任务(Prerequisites)和希望执行的线程(CurrentThreadIfKnown),返回一个事件(ReturnedEventRef)指示该任务已经完成;
Hold——FConstructor会调用该接口,不继续执行,它创建一个完成事件,将自己添加到依赖任务的后续,接受两个参数:依赖的任务(Prerequisites)和希望执行的线程(CurrentThreadIfKnown),返回一个任务对象;
CreateTask——这是CreateTask的另一个重写版本,是一个工厂接口,借助辅助类构造我们的任务对象,与之前不同的是。接收三个参数:依赖于此任务的任务(SubsequentsToAssume)、依赖的任务(Prerequisites)和希望执行的线程类型(CurrentThreadIfKnown),返回一个辅助类(FConstructor)对象;
属性介绍:
TaskStorage——存储通过辅助类(FConstructor)创建的任务对象;
TaskConstructed——类型为bool,从注释和名字来看是标志任务对象是否构建的标志;
Subsequents——类型为FGraphEventRef,是一个指向完成事件的引用计数,记录以此任务为先决条件的任务;

2.FBaseGraphTask:

FBaseGraphTaskTGraphTask的父类,也是TaskGraph系统中所有任务模型的基类,大致源码如下:

class FBaseGraphTask
{
	
	//……
	
	void SetThreadToExecuteOn(ENamedThreads::Type InThreadToExecuteOn)
	{
		//……
	}

	void PrerequisitesComplete(ENamedThreads::Type CurrentThread, int32 NumAlreadyFinishedPrequistes, bool bUnlock = true)
	{
		//……
	}
	
	//……

	void ConditionalQueueTask(ENamedThreads::Type CurrentThread)
	{
		//……
	}

private:
	
	//……

	virtual void ExecuteTask(TArray<FBaseGraphTask*> & NewTasks, ENamedThreads::Type CurrentThread) = 0;

	FORCEINLINE void Execute(TArray<FBaseGraphTask*> & NewTasks, ENamedThreads::Type CurrentThread)
	{
		//……
	}

	void QueueTask(ENamedThreads::Type CurrentThreadIfKnown)
	{
		//……
	}

	ENamedThreads::Type			ThreadToExecuteOn;
	
	FThreadSafeCounter			NumberOfPrerequistitesOutstanding;

	//……

};

接口介绍:
SetThreadToExecuteOn——设置需要的线程,接受一个参数:希望执行任务的线程类型(InThreadToExecuteOn);
PrerequisitesComplete——当依赖的任务设置完成是调用,判断依赖的任务是否完成,若完成此任务即可执行,接收三个参数:当前线程的类型(CurrentThread)、已完成未设置的前置任务数(NumAlreadyFinishedPrequistes)和为真时让执行此任务的标志(bUnlock);
ConditionalQueueTask——每当有一个依赖的任务被完成,未完成前置任务数就减一,如果前置任务以及全部完成,把此任务加入等待队列,接收一个参数:当前线程的类型(CurrentThread);
ExecuteTask——子类需要实现的API,调用该接口真正执行任务,接收两个参数:新任务列表(NewTasks)和执行任务的线程类型(CurrentThread);
Execute——提供给TaskGraph系统的其他部分调用,只是一个对外的接口真正执行任务是调用ExecuteTask接口,接收两个参数:新任务列表(NewTasks)和执行任务的线程类型(CurrentThread);
QueueTask——内部调用函数,添加任务到等待队列,接收一个参数:当前线程类型(CurrentThreadIfKnown)若已知则为当前线程类型否则为任意线程;
属性介绍:
ThreadToExecuteOn——类型为ENamedThreads::Type,表示此任务执行的线程类型;
NumberOfPrerequistitesOutstanding——还没完成的前置数,当数量减为0时把此任务加入等待队列;
介绍到这里,整个系统的架构基本讲完了,这里附一张简略架构图(自己画的水平有限):

UE4 多线程源码浅析(3——TaskGraph),虚幻引擎,ue4,算法,游戏引擎,虚幻,游戏程序

四.TaskGraph系统的初始化:

在引擎启动的时候,虚幻的TaskGraph系统会在FEngineLoop::PreInitPreStartupScreen中初始化,简略代码如下:

注:引擎的启动流程等我后面写文章再仔细讲讲
注:源码文件路径:Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp

FTaskGraphInterface::Startup(FPlatformMisc::NumberOfCores());
FTaskGraphInterface::Get().AttachToThread(ENamedThreads::GameThread);

UE4.25版本的源码中,这两行代码出现在FEngineLoop::PreInitPreStartupScreen函数的第20505021行。首先调用FTaskGraphInterface的Startup接口,传入CPU核的数量(通过FPlatformMisc::NumberOfCores),Startup实现如下:

void FTaskGraphInterface::Startup(int32 NumThreads)
{
	new FTaskGraphImplementation(NumThreads); 
}

很简单,就是new了一个FTaskGraphImplementation(就是之前讲过的系统主体类的表现层)单例对象。
接下来会调用FTaskGraphInterface::Get获取类单例对象,调用对象的AttachToThread接口把游戏现场加入到TaskGraph系统中。

五.实现细节:

从**《UE4 多线程的使用》我们知道,使用TaskGraph需要先调用CreateTask创建一个任务,再用WaitUntilTaskCompletes**执行任务。

a.CreateTask实现如下:

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);
}

static FConstructor CreateTask(FGraphEventRef SubsequentsToAssume, const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
	if (sizeof(TGraphTask) <= FBaseGraphTask::SMALL_TASK_SIZE)
	{
		void *Mem = FBaseGraphTask::GetSmallTaskAllocator().Allocate();
		return FConstructor(new (Mem) TGraphTask(SubsequentsToAssume, Prerequisites ? Prerequisites->Num() : 0), Prerequisites, CurrentThreadIfKnown);
	}
	return FConstructor(new TGraphTask(SubsequentsToAssume, Prerequisites ? Prerequisites->Num() : 0), Prerequisites, CurrentThreadIfKnown);
}

CreateTask有多个重写的版本,调用的时候可以传入前置任务列表(Prerequisites)和后置任务(SubsequentsToAssume),系统会再前置任务完成后执行该任务,再该任务完成后触发后续任务的执行。
函数内部先获取前置任务的数量(NumPrereq),再判断自定义任务的大小是否大于最小值(FBaseGraphTask::SMALL_TASK_SIZE),若小于最小值就调用GetSmallTaskAllocator().Allocate分配一个最小的任务,最后构造一个辅助类(FConstructor)对象返回去。
通过CreateTask获取到辅助类(FConstructor)对象后,调用它的ConstructAndDispatchWhenReadyConstructAndHold获取一个任务对象。

b.WaitUntilTaskCompletes实现如下:

void WaitUntilTaskCompletes(const FGraphEventRef& Task, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
	FGraphEventArray Prerequistes;
	Prerequistes.Add(Task);
	WaitUntilTasksComplete(Prerequistes, CurrentThreadIfKnown);
}

函数接收一个任务对象的引用(Task)和一个形成类型(CurrentThreadIfKnown )。
函数内部创建一个任务列表(Prerequistes),传入的Task加入进去,再带哦用WaitUntilTasksComplete,把任务列表和线程传进去。WaitUntilTasksComplete实现如下:

virtual void WaitUntilTasksComplete(const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread) final override
{
	//……
	ENamedThreads::Type CurrentThread = CurrentThreadIfKnown;
	if (ENamedThreads::GetThreadIndex(CurrentThreadIfKnown) == ENamedThreads::AnyThread)
	{
		bool bIsHiPri = !!ENamedThreads::GetTaskPriority(CurrentThreadIfKnown);
		int32 Priority = ENamedThreads::GetThreadPriorityIndex(CurrentThreadIfKnown);
		//……
		CurrentThreadIfKnown = ENamedThreads::GetThreadIndex(GetCurrentThread());
		CurrentThread = ENamedThreads::SetPriorities(CurrentThreadIfKnown, Priority, bIsHiPri);
	}
	else
	{
		CurrentThreadIfKnown = ENamedThreads::GetThreadIndex(CurrentThreadIfKnown);
		//……
	}

	if (CurrentThreadIfKnown != ENamedThreads::AnyThread && CurrentThreadIfKnown < NumNamedThreads && !IsThreadProcessingTasks(CurrentThread))
	{
		if (Tasks.Num() < 8)
		{
			bool bAnyPending = false;
			for (int32 Index = 0; Index < Tasks.Num(); Index++)
			{
				if (!Tasks[Index]->IsComplete())
				{
					bAnyPending = true;
					break;
				}
			}
			if (!bAnyPending)
			{
				return;
			}
		}

		TGraphTask<FReturnGraphTask>::CreateTask(&Tasks, CurrentThread).ConstructAndDispatchWhenReady(CurrentThread);
		ProcessThreadUntilRequestReturn(CurrentThread);
	}
	else
	{
		if (!FPlatformProcess::SupportsMultithreading())
		{
			bool bAnyPending = false;
			for (int32 Index = 0; Index < Tasks.Num(); Index++)
			{
				if (!Tasks[Index]->IsComplete())
				{
					bAnyPending = true;
					break;
				}
			}
			if (!bAnyPending)
			{
				return;
			}
			//……
		}
		
		FScopedEvent Event;
		TriggerEventWhenTasksComplete(Event.Get(), Tasks, CurrentThreadIfKnown);
	}
}

该函数实现了一下前置任务的调度,首先判断一下传进来的线程类型是否为任意线程,若是则获取优先级,根据优先级和线程类型获取一个合适的线程。
若不是则为命名线程,判断该线程是否正在执行,若是递执行前置任务。若该线程处于空闲状态,判断平台是否支持多线程和前置任务,调用TriggerEventWhenTasksComplete尝试在前置任务完成时触发一个事件。
这里附一张简略版的虚幻多线程全家福架构图(自己画的水平有限):

UE4 多线程源码浅析(3——TaskGraph),虚幻引擎,ue4,算法,游戏引擎,虚幻,游戏程序

六.虚幻多线程小结:

终于写完了,不容易呀。这一篇再次刷新纪录啦,目前为止最长的一篇。虚幻多线程这部分到这里算是讲完了。由于我还是个在校生,受自身水平限制,我自己觉得写的不是很满意,虽然尽力了,但是有些地方还是没讲好,希望读者见谅。后面等我再研究一下,看看写些什么。不说了,最后感谢观看,我们下一篇再见。文章来源地址https://www.toymoban.com/news/detail-799164.html

到了这里,关于UE4 多线程源码浅析(3——TaskGraph)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【虚幻引擎】UE4/UE5插件

    Blank:空白插件,可以从头开始自己定义想要的插件风格和内容,用此模板创建的插件不会有注册或者菜单输入。 BlueprintLibrary:创建一个含有蓝图函数库的插件,此模板函数都是静态全局函数,可以在蓝图中直接调用。 ContentOnly:创建一个只包含内容的空白文件 Editor Toolba

    2024年02月05日
    浏览(75)
  • 【虚幻引擎】UE4优化植被

    在UE4中,我们在做大型的室外场景时,经常会遇到植物过多导致延迟的现象,有时候我们需要在UE4的场景中放置几千几万甚至更多的模型,这些模型具有相同的LOD,并且基础模型都使用同一模型资源。因为模型文件拖入UE4场景中会自动使用Static Mesh Actor来表示,当在程序中放

    2024年02月15日
    浏览(57)
  • ubuntu18.04源码编译安装carla0.9.13,关联UE4.26虚幻引擎账号

    参考博客:https://www.cnblogs.com/chenjian688/p/16624095.html 查看推荐显卡 找到recommended推荐的版本,本机是470版本。 本机是470版本 如果安装失败,需要在安装之前进行 sudo apt-get update 指令 同时为了避免UE和 CARLA 依赖项之间的兼容性问题,使用相同的编译器版本和 C++ runtime library来编

    2024年02月14日
    浏览(56)
  • 【虚幻引擎UE】UE4/UE5 新人科普向

    Unreal Engine是当前最为流行的游戏引擎之一,具有丰富的游戏开发功能和强大的游戏引擎渲染能力。 UE5官方文档:UE5官方文档非常详细,介绍了UE5的各个功能和应用,适合入门学习和深入探究。链接:https://docs.unrealengine.com/5.1/zh-CN/ UE5中文社区:该社区聚集了大量的UE5开发者,

    2024年02月09日
    浏览(79)
  • 【虚幻引擎】UE4/UE5 pak挂载

     找到:D:UEUE_4.27EngineBinariesWin64,  WindowS+R打开CMD命令 运行UnrealPak,运行结果如下      注意如果想要加载Pak内资源,那么这些资源必须是经过Cook的。如果打包的是未Cook的资源,那么即使Pak挂载成功,也不可能会成功加载Pak内资源。  Cook好之后,存储的路径在你的I:DBJ

    2024年02月10日
    浏览(75)
  • 【虚幻引擎】UE4 Spline(样条线)

           样条线Spline在UE中是一个很好用的工具,能够设置物体的跟随移动,也能够设置物体的批量复制,还能够设置一个特殊的模型形状比如圆管,还可以设置特殊的粒子特效,做地形设计等等,只要你想要实现的效果,spline都可以实现。官方也提供了很多的案例,可以参考

    2023年04月10日
    浏览(68)
  • 【虚幻引擎】UE4/UE5科大讯飞文字合成语音

    B站视频链接:https://space.bilibili.com/449549424?spm_id_from=333.1007.0.0   第一步:首先进入讯飞开放平台注册一个账号,然后创建一个 创建一个应用,命名按照你自己的想法来,会产生一个APPID,具体参考UE4如何接入科大讯飞的语音识别_ue4 科大讯飞的语音识别_飞起的猪的博客-CSDN博

    2024年02月13日
    浏览(72)
  • 【虚幻引擎】UE4 FTabmanager实现多窗口

    我们先看源码的实现方式:FTabmanager类实现了创建窗口的不同方法 NewLayout:创建新的布局方式  RegisterTabSpawner:注册在模块启动时的函数,这个就是创建了一个窗口,窗口所放置的内容  RestoreFrom:从窗口中显示 调用独立窗口需要在模块启动的时候调用FGlobalTabmanager::Get()的Re

    2024年02月10日
    浏览(77)
  • 【虚幻引擎】UE4/UE5鼠标点击事件实现物体移动

     在UE4/UE5中,引擎有它自己的一套框架体系,虚幻就是基于这一个框架体系来实现的。其中就有PlayerController(玩家控制器),玩家控制器中就有对鼠标的一系列设置,包括显示鼠标,允许点击事件等。  1.创建PlayerController,命名为MyPlayerController 2.打开MyPlayerController,勾选参数

    2024年02月10日
    浏览(117)
  • 【虚幻引擎】UE4/UE5数字孪生与前端Web页面匹配

            数字孪生是一种多维动态的数字映射,可大幅提高效能。数字孪生是充分利用物理模型、传感器更新、运行历史等数据,集成多学科、多物理量、多尺度、多概率的仿真过程,在虚拟空间中完成对现实体的复制和映射,从而反映物理实体的全生命周期过程。数字孪生

    2024年02月03日
    浏览(80)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包