UE4
虽然沿用了C++11
的标准,但多线程并没有使用std::thread
,而是自己封装了Runnable
、AsyncTask
以及TaskGraph
这三个可以供我们使用的线程。他们的本质相同但用法不同,使用Runnable
可以自己创建复写线程函数的接口,AsyncTask
是基于Runnable
的一层封装,使用会更加方便。而TaskGraph
则是基于任务的多线程(Task-Based
),可以有效控制任务的顺序性。接下来将简单介绍这三种线程的使用。
Runnable
是UE4
中最基本的多线程,通过继承复写接口来使用。
FRunnable
源码:
class CORE_API FRunnable
{
public:
virtual bool Init()
{
return true;
}
virtual uint32 Run() = 0;
virtual void Stop() { }
virtual void Exit() { }
virtual class FSingleThreadRunnable* GetSingleThreadInterface( )
{
return nullptr;
}
virtual ~FRunnable() { }
};
其中函数的作用从名字就可以看出来了,必须要复写的接口就是Run
(线程运行的逻辑函数)。
于是我们可以写下我们自己的派生类继承于FRunnable
。
class CPPDELEGATEANDSUBSYS_API FRunnableTest:public FRunnable
{
public:
virtual bool Init()override;
virtual uint32 Run() override;
virtual void Exit()override;
};
创建方法:
FRunnableTest* threadPtr = new FRunnableTest(); //不参加GC需要自行delete,可以用智能指针进行管理
FRunnableThread::Create(threadPtr,TEXT("thread01"));
通过FRunnableThread
这个真正的线程中的静态函数Create
进行创建。
// Create参数
static FRunnableThread* Create(
class FRunnable* InRunnable,
const TCHAR* ThreadName,
uint32 InStackSize = 0,
EThreadPriority InThreadPri = TPri_Normal,
uint64 InThreadAffinityMask = FPlatformAffinity::GetNoAffinityMask(),
EThreadCreateFlags InCreateFlags = EThreadCreateFlags::None);
同时线程同步的问题,在UE4
中的解决方案是,使用临界区变量、线程安全属性ESPMode::ThreadSefe
。
//临界区变量的定义以及声明
static FCriticalSection _LockSection;
FScopeLock Lock(&_LockSection);
AsyncTask
是基于线程池的异步任务处理系统。和Runnable
不同的是,Runnable
在底层调用的是WindowsAPI
的CreateThread
,而AsyncTask
是直接在UE
给我们创建好的线程池中获得一个闲置的线程,使用AsyncTask
我们需要继承于FNonAbandonableTask
。字面理解无法被抛弃的任务,指的是在线程执行过程中无法被终止。
class FMyAsyncTask : public FNonAbandonableTask
{
// 可以自动销毁
friend class FAutoDeleteAsyncTask<FMyAsyncTask>;
// 需要手动销毁
// friend class FAsyncTask;
public:
// 执行函数
void DoWork();
// 获取StatId
FORCEINLINE TStatId GetStatId()const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FMyAsyncTask,STATGROUP_ThreadPoolAsyncTasks);
}
};
其中我们必须要将FAutoDeleteAsyncTask
或者FAsyncTask
作为友元类,一个创建Task
后需要手动删除,一个可以自动删除。
创建方式:
// FAutoDeleteAsyncTask作为友元类
FAsyncTask<FMyAsyncTask>*Task = new FAsyncTask<FMyAsyncTask>();
Task->StartBackgroundTask(); //后台运行,从线程池中调用
//Task->StartSynchronousTask(); //从当前线程运行
// FAsyncTask作为友元类
FAsyncTask<FMyAsyncTask>* Task = new FAsyncTask<FMyAsyncTask>();
Task->StartSynchronousTask();
if(Task->IsDone())
{
UE_LOG(LogTemp,Warning,TEXT("Task is Done"));
}
// 模拟同步操作,主线程需要等待子线程完成任务之后才能继续执行。EnsureCompletion在代码底层封装的是信号量
Task->EnsureCompletion();
delete Task;
Task = nullptr;
其中如果使用FAsyncTask
作为友元类,则需要自己手动删除。IsDone()
判断任务有没有执行完成,EnsureCompletion()
确保任务执行完成。
TaskGrap
于传统的基于线程(Thread-Based
)的并行编程不同的是,他是基于任务(Task-Based)
的并行编程。那么什么是基于任务的并行编程呢?可以简单慨括,将流程处理规划成"任务处理"。通过TaskGraph
我们可以着重于某单个任务是需要并行还是需要串行,我们并不需要关心threading
内部的问题,比如死锁、线程同步。引擎自会帮我们管理线程池等底层对象。
class FGraphTaskTest
{
public:
// 获取StatId
FORCEINLINE TStatId GetStatId()const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FGraphTaskTest,STATGROUP_TaskGraphTasks);
}
// 获取该Task运行的线程
static ENamedThreads::Type GetDesiredThread()
{
return ENamedThreads::AnyThread;
}
// 获取Task依赖模式
static ESubsequentsMode::Type GetSubsequentsMode()
{
return ESubsequentsMode::TrackSubsequents;
}
// Task逻辑函数
void DoTask(ENamedThreads::Type CurrentThread,const FGraphEventRef& CompletionEventRef);
};
GetDesiredThread()
函数可以设置Task
运行在哪个线程里面。GetSubsequentsMode()
设置Task
的依赖性。
ESubsequentsMode::FireAndForget; //没有依赖性
ESubsequentsMode::TrackSubsequents; //依赖于子任务
通过依赖性可以实现任务的同步操作。DoTask()
中传入的参数:1.当前任务运行的线程,2.FGraphEvent
为当前任务的句柄,配合句柄可以对任务做出一些扩展操作。
创建方式:
static FConstructor CreateTask(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
// 1.Prerequisites 前置任务数组
// 2.CurrentThreadIfKnown 运行线程
//1.通过ConstructAndDispatchWhenReady(延迟构造)进行构造 后面传入的参数是延迟构造的时间以及class的构造
FGraphEventRef Event = TGraphTask<FGraphTaskTest>::CreateTask(nullptr,ENamedThreads::AnyThread).ConstructAndDispatchWhenReady();
//2.通过ConstructAndHold进行构造(构造了但不会执行),需要weakup唤醒
TGraphTask<class FGraphTaskTest> *TaskPtr = TGraphTask<FGraphTaskTest>::CreateTask(nullptr,ENamedThreads::AnyThread).ConstructAndHold();
// 唤醒
TaskPtr->Unlock();