详细参考:《Exploring in UE4》多线程机制详解[原理分析] - 知乎 (zhihu.com)
UE4 C++基础 - 多线程 - 知乎 (zhihu.com)
标准模板
//Runnable.h
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() { }
};
实际上,在实现多线程的时候,我们需要将FRunnable作为参数传递到真正的线程里面,然后才能通过线程去调用FRunnable的Run,也就是我们具体实现的类的Run方法(通过虚函数覆盖父类的Run)。所谓真正的线程其实就是FRunnableThread,不同平台的线程都继承自他
或者
UE4是跨平台的引擎,对各个平台线程实现进行了封装,抽象出了 FRunnable 。引擎中大部分的需要多线程执行逻辑都是继承这个类实现的多线程
#include "HAL/Runnable.h"
class MyRunnable : public FRunnable {
public:
virtual bool Init() override; // 初始化 runnable 对象
virtual uint32 Run() override; // 运行 runnable 对象
virtual void Stop() override; // 停止 runnable 对象,线程提前终止时被调用
virtual void Exit() override; // 退出 runnable 对象
};
bool MyRunnable::Init() { return true; }
uint32 MyRunnable::Run() { return 0; }
void MyRunnable::Stop() {}
void MyRunnable::Exit() {}
调用顺序是 Init(), Run(), Exit()。Runnable对象初始化操作在 Init() 函数中完成,并通过返回值确定是否成功。初始化失败,则该线程停止执行,并返回一个错误码;成功,则会执行 Run() ;执行完毕后,则会调用 Exit() 执行清理操作。
Runnable负责具体业务逻辑的执行,UE4中使用 FRunnableThread 表示一个可执行的线程。 可以通过调用 FRunnableThread::Create 完成线程的创建:
#include "HAL/RunnableThread.h"
static FRunnableThread * Create
(
class FRunnable * InRunnable, // Runnable 对象
const TCHAR * ThreadName, // 线程名称
uint32 InStackSize, // 线程栈大小,0表示使用当前线程的栈大小
EThreadPriority InThreadPri, // 线程优先级
uint64 InThreadAffinityMask
);
// 返回值:若成功则返回创建的线程,否则返回 nullptr
样例代码如下:
#include "HAL/RunnableThread.h"
FRunnable * Runnable = new MyRunnable();
FRunnableThread* RunnableThread = FRunnableThread::Create(Runnable, TEXT("LaLaLaDeMaXiYa!"));
线程对象创建成功后即开始执行Runnable对象的 Init () 函数,如果成功则分别执行Run() 和 Exit() 函数。
每个线程都有一个线程ID,线程ID在它所属的进程环境中有效。为增加标识性,UE4还增加了线程名称。线程ID是唯一的,线程名称可以重复。可通过GetThreadID 和 GetThreadName 获取线程ID和名称。
const uint32 GetThreadID() const;
const FString & GetThreadName() const;
单个线程可以通过如下三种方式退出。
- 线程执行完 runnable 对象的 Run() 和 Exit() 函数后正常退出
- 调用 WaitForCompletion() 函数,阻塞调用例程直到线程执行完毕
- 调用 Kill(bool bShouldWait=false) 函数,会先执行 runnable 对象的 stop 函数,然后根据 bShouldWait 参数决定是否等待线程执行完毕。如果不等待,则强制杀死线程,可能会造成内存泄漏。
void WaitForCompletion(); // 阻塞调用例程,直到线程执行完毕
bool Kill(bool bShouldWait); // 强制杀掉线程
通过FRunnableThread 创建的线程是通过 FThreadManager 进行统一管理。
// ThreadingBase.cpp FRunnableThread::Create 函数
// Call the thread's create method
if (NewThread->CreateInternal(InRunnable,ThreadName,InStackSize,InThreadPri,InThreadAffinityMask) == false)
CreateInternal根据平台的不同实现不同,常用平台中,Android和iOS都是采用的 pthread标准线程库,Windows平台是单独实现的。线程创建完毕后会统一调用
FThreadManager::Get().AddThread(ThreadID, this);
将线程本身添加至管理器。如 WindowsRunnableThread.h FRunnableThreadWin::CreateInternal 函数。标准线程对象 FRunnableThreadPThread 则是在入口点:
virtual PthreadEntryPoint GetThreadEntryPoint() {
return _ThreadProc;
}
static void *STDCALL _ThreadProc(void *pThis) {
check(pThis);
FRunnableThreadPThread* ThisThread = (FRunnableThreadPThread*)pThis;
// cache the thread ID for this thread (defined by the platform)
ThisThread->ThreadID = FPlatformTLS::GetCurrentThreadId();
// ====================>>这里将线程本身加入管理器 <<==========================
FThreadManager::Get().AddThread(ThisThread->ThreadID, ThisThread);
// set the affinity. This function sets affinity on the current thread, so don't call in the Create function which will trash the main thread affinity.
FPlatformProcess::SetThreadAffinityMask(ThisThread->ThreadAffinityMask);
// run the thread!
ThisThread->PreRun();
ThisThread->Run();
ThisThread->PostRun();
pthread_exit(NULL);
return NULL;
}
线程过多会带来调度开销,进而影响缓存局部性和整体性能。频繁创建和销毁线程也会带来极大的开销。通常我们更加关心的是任务可以并发执行,并不想管理线程的创建,销毁和调度。通过将任务处理成队列,交由线程池统一执行,可以提升任务的执行效率。UE4提供了对应的线程池来满足我们的需求。异步任务统一都继承至 IQueuedWork,属于抽象接口类,可供我们直接使用的是
异步任务通常继承 FNonAbandonableTask,表明该任务不可被抛弃,必须被执行完毕。样例代码如下:
idi#include "Async/AsyncWork.h"
class ExampleAsyncTask : public FNonAbandonableTask
{
friend class FAsyncTask;
friend class FAutoDeleteAsyncTask;
int32 ExampleData;
ExampleAsyncTask(int32 InExampleData)
: ExampleData(InExampleData){}
void DoWork() {
UE_LOG(LogBlankProgram, Display, TEXT("ExampleAsyncTask %d Work."), ExampleData);
}
FORCEINLINE TStatId GetStatId() const {
RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
}
};
void Example {
// 2.1 线程池异步队列
FAsyncTask* MyTask = new FAsyncTask(1);
// 交由后台控制任务开始执行时机
MyTask->StartBackgroundTask();
// 确保线程被执行完成
MyTask->EnsureCompletion();
delete MyTask;
}
AsyncTask系统是一套基于线程池的异步任务处理系统,样例如下:
//AsyncWork.h
class ExampleAsyncTask : public FNonAbandonableTask
{
friend class FAsyncTask;
int32 ExampleData;
ExampleAsyncTask(int32 InExampleData)
: ExampleData(InExampleData)
{
}
void DoWork()
{
... do the work here
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
}
};
void Example()
{
//start an example job
FAsyncTask* MyTask = new FAsyncTask( 5 );
MyTask->StartBackgroundTask();
//--or --
MyTask->StartSynchronousTask();
//to just do it now on this thread
//Check if the task is done :
if (MyTask->IsDone())
{
}
//Spinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame.
//Ensure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases.
MyTask->EnsureCompletion();
delete Task;
}
FQueuedThreadPool,和一般的线程池实现类似,线程池里面维护了多个线程FQueuedThread与多个任务队列IQueuedWork,线程是按照队列的方式来排列的。在引擎PreInit的时候执行相关的初始化操作,代码如下
// FEngineLoop.PreInit LaunchEngineLoop.cpp
if (FPlatformProcess::SupportsMultithreading())
{
{
GThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfWorkerThreadsToSpawn();
// we are only going to give dedicated servers one pool thread
if (FPlatformProperties::IsServerOnly())
{
NumThreadsInThreadPool = 1;
}
verify(GThreadPool->Create(NumThreadsInThreadPool, 128 * 1024));
}
#ifUSE_NEW_ASYNC_IO
{
GIOThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfIOWorkerThreadsToSpawn();
if (FPlatformProperties::IsServerOnly())
{
NumThreadsInThreadPool = 2;
}
verify(GIOThreadPool->Create(NumThreadsInThreadPool, 16 * 1024, TPri_AboveNormal));
}
#endif// USE_NEW_ASYNC_IO
#ifWITH_EDITOR
// when we are in the editor we like to do things like build lighting and such
// this thread pool can be used for those purposes
GLargeThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInLargeThreadPool = FMath::Max(FPlatformMisc::NumberOfCoresIncludingHyperthreads() - 2, 2);
verify(GLargeThreadPool->Create(NumThreadsInLargeThreadPool, 128 * 1024));
#endif
}
专有服务器的线程池GThreadPool默认只开一个线程,非专有服务器的根据核数开(CoreNum-1)个线程。编辑器模式会另外再创建一个线程池GLargeThreadPool,包含(LogicalCoreNum-2)个线程,用来处理贴图的压缩和编码相关内容。
在线程池里面所有的线程都是FQueuedThread类型,不过更确切的说FQueuedThread是继承自FRunnable的线程执行体,每个FQueuedThread里面包含一个FRunnableThread作为内部成员。
相比一般的线程,FQueuedThread里面多了一个成员FEvent* DoWorkEvent,也就是说FQueuedThread里面是有一个事件触发机制的。那么这个事件机制的作用是什么?一般情况下来说,就是在没有任务的时候挂起这个线程,在添加并分配给该线程任务的时候激活他,不过你可以灵活运用它,在你需要的时候去动态控制线程任务的执行与暂停。前面我们在给线程池初始化的时候,通过FQueuedThreadPool的Create函数创建了多个FQueuedThread,然后每个FQueuedThread会执行Run函数,里面有一段逻辑如下:
//ThreadingBase.cpp
bool bContinueWaiting = true;
while(bContinueWaiting )
{
DECLARE_SCOPE_CYCLE_COUNTER(TEXT( "FQueuedThread::Run.WaitForWork" ), STAT_FQueuedThread_Run_WaitForWork, STATGROUP_ThreadPoolAsyncTasks );
// Wait for some work to do
bContinueWaiting = !DoWorkEvent->Wait( 10 );
}
//windows平台下的wait
bool FEventWin::Wait(uint32 WaitTime, const bool bIgnoreThreadIdleStats/*= false*/)
{
WaitForStats();
SCOPE_CYCLE_COUNTER(STAT_EventWait );
check(Event );
FThreadIdleStats::FScopeIdleScope(bIgnoreThreadIdleStats );
return (WaitForSingleObject( Event, WaitTime ) == WAIT_OBJECT_0);
}
我们看到,当DoWorkEvent执行Wait的时候,如果该线程的Event处于无信号状态(默认刚创建是无信号的),那么wait会等待10毫秒并返回false,线程处于While无限循环中。如果线程池添加了任务(AddQueuedWork)并执行了DoWorkEvent的Trigger函数,那么Event就会被设置为有信号,Wait函数就会返回true,随后线程跳出循环进而处理任务。
注:FQueuedThread里的DoWorkEvent是通FPlatformProcess::GetSynchEventFromPool();从EventPool里面获取的。WaitForSingleObject等内容涉及到Windows下的事件机制,大家可以自行到网上搜索相关的使用,这里给出一个官方的 使用案例。
线程池的任务IQueuedWork本身是一个接口,所以得有具体实现。这里你就应该能猜到,所谓的AsynTask其实就是对IQueuedWork的具体实现。这里AsynTask泛指FAsyncTask与FAutoDeleteAsyncTask两个类,我们先从FAsyncTask说起。
FAsyncTask有几个特点,
- FAsyncTask是一个模板类,真正的AsyncTask需要你自己写。通过DoWork提供你要执行的具体任务,然后把你的类作为模板参数传过去
- 使用FAsyncTask就默认你要使用UE提供的线程池FQueuedThreadPool,前面代码里说明了在引擎PreInit的时候会初始化线程池并返回一个指针GThreadPool。在执行FAsyncTask任务时,如果你在执行StartBackgroundTask的时候会默认使用GThreadPool线程池,当然你也可以在参数里面指定自己创建的线程池
- 创建FAsyncTask并不一定要使用新的线程,你可以调用函数StartSynchronousTask直接在当前线程上执行任务
- FAsyncTask本身包含一个DoneEvent,任务执行完成的时候会激活该事件。当你想等待一个任务完成时再做其他操作,就可以调用EnsureCompletion函数,他可以从队列里面取出来还没被执行的任务放到当前线程来做,也可以挂起当前线程等待DoneEvent激活后再往下执行
FAutoDeleteAsyncTask与FAsyncTask是相似的,但是有一些差异,
- 默认使用UE提供的线程池FQueuedThreadPool,可以通过参数指定使用其他线程池
- FAutoDeleteAsyncTask在任务完成后会通过线程池的Destroy函数删除自身或者在执行DoWork后删除自身,而FAsyncTask需要手动delete
- 包含FAsyncTask的特点1和特点3
总的来说,AsyncTask系统实现的多线程与你自己字节继承FRunnable实现的原理相似,不过他在用法上比较简单,而且还可以直接借用UE4提供的线程池,很方便。
当多个线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取或者修改的,那么就不存在一致性问题。同样地,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是,当某个线程可以修改变量,而其他线程也可以读取或者修改这个变量的时候,就需要对这些线程进行同步,以确保它们在访问变量的存储内容时不会访问到无效的数值。这个时候就需要用到线程同步机制。
UE4提供了以下几个不同类别的同步机制:
Atomic operations(原子操作) 保证CPU在读取和写入内存时总线操作是不可分割的。它是许多高级同步机制的基础,主要优势是可以进行比较快的进行比较和解锁操作。一个用Atomics实现的样例如下:
class FThreadSafeCounter{
public:
int32 Add( int32 Amount ) {
return FPlatformAtomics::InterlockedAdd(&Counter, Amount);
}
private:
volatile int32 Counter; // 因为值可能以编译器无法预测的异步方式被改变,声明为volatile禁用优化
};
在UE4中常用的两种锁机制是 Critical Sections(临界区)和 SpinLocks(自旋锁)。
{
FScopedEvent MyEvent;
SendReferenceOrPointerToSomeOtherThread(&MyEvent); // Other thread calls MyEvent->Trigger() ;
// MyEvent destructor is here, we wait here.
}
其中 FCriticalSection 是根据各个平台的互斥锁进行的抽象。Windows 平台是基于Windows平台的临界区。常用的iOS, Android,Linux平台则是使用的POSIX的线程标准实现[13]。
UE4常见的容器类【TArray, TMap, TSet】通常都不是线程安全的,需要我们仔细编写代码保证线程安全。下面是几个常见的线程安全类:
下面是一个简单的线程安全TSet,附带FCriticalSection使用示例。
/** Simple thread safe proxy for TSet */
template
class FThreadSafeSet
{
TSet InnerSet;
FCriticalSection SetCritical;
public:
void Add(T InValue) {
FScopeLock SetLock(&SetCritical);
InnerSet.Add(InValue);
}
bool AddUnique(T InValue) {
FScopeLock SetLock(&SetCritical);
if (!InnerSet.Contains(InValue))
{
InnerSet.Add(InValue);
return true;
}
return false;
}
bool Contains(T InValue) {
FScopeLock SetLock(&SetCritical);
return InnerSet.Contains(InValue);
}
void Remove(T InValue) {
FScopeLock SetLock(&SetCritical);
InnerSet.Remove(InValue);
}
void Empty() {
FScopeLock SetLock(&SetCritical);
InnerSet.Empty();
}
void GetValues(TSet& OutSet) {
FScopeLock SetLock(&SetCritical);
OutSet.Append(InnerSet);
}
int32 Num() { return InnerSet.Num();}
};
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
#include "BlankProgram.h"
#include "RequiredProgramMainCPPInclude.h"
#include "HAL/Runnable.h"
#include "HAL/RunnableThread.h"
#include "Async/AsyncWork.h"
DEFINE_LOG_CATEGORY_STATIC(LogBlankProgram, Log, All);
IMPLEMENT_APPLICATION(BlankProgram, "BlankProgram");
class MyRunnable : public FRunnable {
public:
virtual bool Init() override; // 初始化 runnable 对象
virtual uint32 Run() override; // 运行 runnable 对象
virtual void Stop() override; // 停止 runnable 对象,线程提前终止时被调用
virtual void Exit() override; // 退出 runnable 对象
};
bool MyRunnable::Init() {
UE_LOG(LogBlankProgram, Display, TEXT("Thread Init."));
return true;
}
uint32 MyRunnable::Run() {
UE_LOG(LogBlankProgram, Display, TEXT("Thread Run."));
return 0;
}
void MyRunnable::Stop() {}
void MyRunnable::Exit() {
UE_LOG(LogBlankProgram, Display, TEXT("Thread Exit."));
}
// 任务队列
class ExampleAsyncTask : public FNonAbandonableTask {
friend class FAsyncTask;
friend class FAutoDeleteAsyncTask;
int32 ExampleData;
ExampleAsyncTask(int32 InExampleData)
: ExampleData(InExampleData){}
void DoWork() {
UE_LOG(LogBlankProgram, Display, TEXT("ExampleAsyncTask %d Work."), ExampleData);
}
FORCEINLINE TStatId GetStatId() const {
RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
}
};
INT32_MAIN_INT32_ARGC_TCHAR_ARGV()
{
GEngineLoop.PreInit(ArgC, ArgV);
UE_LOG(LogBlankProgram, Display, TEXT("UE4 Multithreading Example."));
// 1. FRunnable 使用示例
FRunnable * Runnable = new MyRunnable();
FRunnableThread* RunnableThread = FRunnableThread::Create(Runnable, TEXT("LaLaLaDeMaXiYa!"));
RunnableThread->WaitForCompletion();
// 2.1 线程池异步队列
FAsyncTask* MyTask = new FAsyncTask(1);
// 交由后台控制任务开始执行时机
MyTask->StartBackgroundTask();
// 确保线程被执行完成
MyTask->EnsureCompletion();
delete MyTask;
// 2.2 线程池异步队列
FAsyncTask* MyTask2 = new FAsyncTask(2);
// 直接在当前线程中执行
MyTask2->StartSynchronousTask();
// 检查任务是否完成
if (MyTask2->IsDone()) {
UE_LOG(LogBlankProgram, Display, TEXT("MyTask2 is Done."));
}
MyTask2->EnsureCompletion();
delete MyTask2;
// 2.3 带自动销毁的异步任务
// 交由后台控制任务开始执行时机
(new FAutoDeleteAsyncTask(3))->StartBackgroundTask();
// 直接在当前线程中开始执行
(new FAutoDeleteAsyncTask(4))->StartSynchronousTask();
return 0;
}