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

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

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

被依赖的任务指向依赖的任务(例如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时把此任务加入等待队列;
介绍到这里,整个系统的架构基本讲完了,这里附一张简略架构图(自己画的水平有限):

在这里插入图片描述

四.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尝试在前置任务完成时触发一个事件。
这里附一张简略版的虚幻多线程全家福架构图(自己画的水平有限):

在这里插入图片描述

六.虚幻多线程小结:

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

  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 在UE4中,可以通过使用多线程来运行插件,以提高程序的性能和并行处理能力。 首先,插件在UE4中是以模块的形式存在的,可以独立于游戏项目而存在。为了实现多线程运行插件,可以在插件的模块中编写异步任务,利用多线程来执行这些任务。 在UE4中,我们可以使用FRunnable接口来创建自定义线程。首先,需要创建一个继承自FRunnable的类,并实现必要的接口函数,如Run和Stop。在Run函数中,编写插件需要执行的任务代码。 在插件的初始化阶段,可以通过调用FRunnableThread::Create函数来创建一个新的线程,并指定我们刚刚定义的FRunnable类对象作为参数。然后,可以开始执行插件任务。 需要注意的是,在多线程编程中,需要合理地处理线程之间的数据共享和同步问题。可以使用一些线程同步的机制,如互斥锁、信号量等,来避免多个线程同时操作共享数据导致的冲突。 另外,UE4还提供了一些已经封装好的多线程工具类,如FGraphEvent和FQueuedThread等,可以帮助我们更方便地实现多线程任务的管理和同步。 总结来说,UE4中可以通过使用FRunnable接口和多线程工具类来实现插件的多线程运行。合理地设计和管理多线程任务,可以提高程序的性能和并行处理能力,并且确保线程之间的数据共享和同步的正确性。 ### 回答2: 在UE4中,我们可以通过多线程来实现插件的运行。多线程是一种并发执行任务的方式,可以让程序同时执行多个任务,提高了程序的效率和性能。 在UE4中,我们可以使用一些多线程的技术来实现插件的运行。比如,使用C++中的std::thread来创建多个线程,并让每个线程在后台执行插件相关的任务。同时,我们还可以使用一些线程同步的机制,比如互斥锁、条件变量等来管理多个线程之间的资源访问和共享。 在插件的运行过程中,可以将一些耗时的任务放在单独的线程中执行,这样就不会阻塞主线程的执行。比如,可以将插件中的某个算法或者处理逻辑放在一个独立的线程中运行,这样主线程可以继续执行其他的任务,提高了程序的响应速度和用户体验。 除了使用多线程,还可以使用UE4中提供的任务图谱系统来实现插件的并行执行。任务图谱系统可以将任务划分成多个小任务,并按照依赖关系进行调度,从而充分利用计算资源并提高任务执行的效率。 总结来说,UE4中可以通过多线程技术来实现插件的运行,可以提高程序的效率和性能,同时还可以使用任务图谱系统来实现任务的并行执行。这些技术可以充分发挥计算资源的利用率,提高插件的运行效果和用户体验。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值