本文章只是我个人在学习虚幻引擎过程中的一些理解,不一定正确,若有说的不对的地方,欢迎指正。
一.基本介绍
在现代操作系统中,多线程是一项极其重要的特性。因此基于多线程平台开发出来的程序应该要好好的利用这种特性。虚幻引擎作为跨平台的、大型游戏引擎,也提供了一套自己的使用平台多线程的解决方案,首先来了解一下虚幻多线程的基本概念。
虚幻多线程分为专用线程和线程池线程,
专用线程——游戏线程(GameThread)、渲染指令生成线程(RenderThread)、渲染指令执行线程(RHIThread)、音视频处理线程(AudioThread)等;
线程池线程——这种线程是在引擎初始化过程中创建的,在引擎启动的过程中会创建一个线程池,在创建一些线程放在里面,并且使用创建出来的线程加速引擎的初始化,引擎初始化完成后这些线程并不会被销毁,而是保存下来留待后续有需要时使用。
注:虚幻引擎启动部分以后再写文章介绍
基于这些概念,虚幻引擎提供了三种主要的使用多线程的工具:FRunnable、AsyncTask、TaskGraph。
二.FRunnable:
FRunnable是三种工具中使用方法最简单的,它会重新创建一个线程来执行你的逻辑,虚幻引擎的专用线程就是用它来创建的。FRunnable的使用主要借助两个类:FRunnable和FRunnableThread,
FRunnable——可以看作是逻辑的载体;
FRunnableThread——可以看作是真正的线程,我们使用它来执行写再FRunnable里的逻辑。
使用步骤如下:
1.我们需要写一个继承自FRunnable的子类,示例如下:
class FTestRunnable : public FRunnable
{
//……
};
2.在子类中重写几个接口,示例如下:
class FTestRunnable : public FRunnable
{
virtual bool Init()
{
//线程初始化的时候执行
return true;
}
virtual uint32 Run()
{
//线程执行的逻辑
return 0;
}
virtual void Exit()
{
//线程退出的时候执行
}
};
这几个接口中,Run一定要重写,否则会编译报错,其他几个可选。
3.启动线程,示例如下:
FTestRunnable MyRunnable;
FRunnableThread::Create(&MyRunnable, TEXT("TestThread"), 0, TPri_Normal);
先创建一个FTestRunnable实例,再用FRunnableThread内的Create函数启动线程,传入承载执行逻辑的类实例,新线程的名字,线程栈大小和线程优先级。线程会自动调用Init,然后是Run,在退出的时候再调用Exit。
三.AsyncTask:
AsyncTask是基于虚幻线程池线程实现的异步任务系统,它使用线程池内的空闲线程来执行你的逻辑。异步任务系统的使用借助FNonAbandonableTask、FAsyncTask和FAutoDeleteAsyncTask:
FNonAbandonableTask是逻辑的载体。FAsyncTask和FAutoDeleteAsyncTask都用来启动线程,区别就像这两个类的名字,线程结束后FAutoDeleteAsyncTask会自动删除自身实例而FAsyncTask则需要手动删除。
使用步骤如下:
1.我们需要写一个继承自FNonAbandonableTask的子类,示例如下:
class FTestTask : public FNonAbandonableTask
{
//……
};
2.在子类中重写几个接口,示例如下:
class FTestTask : public FNonAbandonableTask
{
friend class FAutoDeleteAsyncTask<FTestTask>;
//friend class FAsyncTask<FTestTask>;
//可以再类内写上执行线程需要的数据,通过FTestTask构造函数初始化,示例如下:
/*
int32 TestData;
FTestTask(int32 InData)
:TestData(InData)
{}
*/
void DoWork()
{
//线程执行的逻辑
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FTestTask, STATGROUP_ThreadPoolAsyncTasks);
}
};
类内先声明启动线程类的友元(FAsyncTask或FAutoDeleteAsyncTask)。实现GetStatId,写法很固定。再实现一下DoWork,DoWork就是我们写线程逻辑的地方。
3.启动线程,示例如下:
FAutoDeleteAsyncTask<FTestTask>* MyTask = new FAutoDeleteAsyncTask<FTestTask>(/*可以在这里传入FTestTask需要的参数*/);
//FAsyncTask<FTestTask>()* MyTask = new FAsyncTask<FTestTask>(/*可以在这里传入FTestTask需要的参数*/)
MyTask->StartBackgroundTask();
//MyTask->StartSynchronousTask();
我们需要new一个之前声明的启动线程类的实例(就是声明友元的那个),在调用实例的StartBackgroundTask或者StartSynchronousTask方法,区别如下:
StartBackgroundTask——重新在线程池里找一个空闲线程执行;
StartSynchronousTask——直接使用当前线程执行。
四.TaskGraph:
日常生活中,我们经常遇见在一个完成需要之前需要先完成其他任务。在程序开发中,我们也时常会遇到相似的情况,因此虚幻引擎为我们提供了一个解决这种需求的解决方案——TaskGraph。
TaskGraph直译过来就是任务图系统,叫这个名字可能是因为任务与任务的相互依赖的关系构成了一张有向图。TaskGraph的使用需要自己实现一个逻辑载体类和启动线程的TGraphTask类。
使用步骤如下:
1.实现逻辑载体类,示例如下:
class FTestGraphTask
{
public:
//可以再类内写上执行线程需要的数据,通过FTestTask构造函数初始化,示例如下:
/*
int32 TestData;
FTestGraphTask(int32 InData)
:TestData(InData)
{}
*/
FORCEINLINE static TStatId GetStatId()
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FTestGraphTask, STATGROUP_TaskGraphTasks);
}
static ENamedThreads::Type GetDesiredThread()
{
return ENamedThreads::AnyThread;
}
static ESubsequentsMode::Type GetSubsequentsMode()
{
return ESubsequentsMode::TrackSubsequents;
//return ESubsequentsMode::FireAndForget;
}
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
//……
}
};
实现的类中需要几个固定操作:
GetStatId——这个很熟悉了吧,跟之前一样,把RETURN_QUICK_DECLARE_CYCLE_STAT的第一个参数写上自己类的名字即可;
GetDesiredThread——该任务使用什么线程执行,返回ENamedThreads枚举值,常用的值包括几个专用线程(GameThread、RenderThread、RHIThread、AudioThread)、任意线程(AnyThread)等;
GetSubsequentsMode——该任务是否可以被依赖,返回ESubsequentsMode枚举值,他只有两个选择,可以被依赖(TrackSubsequents)和不可被依赖(FireAndForget);
DoTask——跟前面一样,写我们需要线程执行的逻辑。
2.启动线程,示例如下:
FGraphEventRef MyGraphEventRef = TGraphTask<FTestGraphTask>::CreateTask(nullptr).ConstructAndDispatchWhenReady();
//TGraphTask<FTestGraphTask>::CreateTask(nullptr).ConstructAndHold();
CreateTask的第一个参数就是此次任务所依赖的前置任务,我们可以传入一个指向FGraphEventArray类对象的指针。
再来看看两种启动方法:
ConstructAndDispatchWhenReady——构建后当前置任务完成后触发执行,返回一个后续事件的引用(FGraphEventRef );
ConstructAndHold——构建后挂起,等待触发。
3.触发后续任务,示例如下:
ConstructAndDispatchWhenReady返回的应用(FGraphEventRef )用来触发后续依赖此任务的任务开始执行,示例代码如下
FTaskGraphInterface::Get().WaitUntilTaskCompletes(MyGraphEventRef, ENamedThreads::AnyThread);
WaitUntilTaskCompletes的第一个参数就是前面返回的引用,第二个是当前线程的类型,因为前面的GetDesiredThread我们返回的是ENamedThreads::AnyThread,所以这里我们传入系统的枚举值.
五.小结:
本篇仅仅讲了虚幻引擎提供的几种多线程工具的用法,比较简单,后面等我再研究一下它们的实现,再写文章讲讲。感谢观看,我们下一篇再见。