UE 多线程
详细参考:《Exploring in UE4》多线程机制详解[原理分析] - 知乎 (zhihu.com)
UE4 C++基础 - 多线程 - 知乎 (zhihu.com)
多线程的好处
- 通过为每种事件类型的处理分配单独的线程,能够简化处理异步事件的代码。每个线程在进行事件处理时可以采用同步编程模式,同步编程模式要比异步编程模式简单得多。
- 多个进程必须使用操作系统提供的复杂机制才能实现内存和文件描述符的共享。而多个线程自动地可以访问相同的储存地址空间和文件描述符。
- 有些问题可以通过将其分解从而改善整个程序的吞吐量。在只有一个控制线程的情况下,单个进程需要完成多个任务时,实际上需要把这些任务串行化;有了多个控制线程,相互独立的任务的处理就可以交叉进行,只需要为每个任务分配一个单独的线程,当然只有在处理过程互不依赖的情况下,两个任务的执行才可以穿插进行。
- 交互的程序同样可以通过使用多线程实现响应时间的改善,多线程可以把程序中处理用户输入输出的部分与其他部分分开。
UE4中的多线程
FRunnable
标准模板
//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() 执行清理操作。
FRunnableThread
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); // 强制杀掉线程
FThreadManager
通过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,属于抽象接口类,可供我们直接使用的是
- FAsyncTask 异步任务,自动加入线程池
- FAutoDeleteAsyncTask 异步任务,任务完成后会自动销毁
异步任务通常继承 FNonAbandonableTask,表明该任务不可被抛弃,必须被执行完毕。样例代码如下:
idi#include "Async/AsyncWork.h"class ExampleAsyncTask : public FNonAbandonableTask
{friend class FAsyncTask<ExampleAsyncTask>;friend class FAutoDeleteAsyncTask<ExampleAsyncTask>;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<ExampleAsyncTask>* MyTask = new FAsyncTask<ExampleAsyncTask>(1);// 交由后台控制任务开始执行时机MyTask->StartBackgroundTask();// 确保线程被执行完成MyTask->EnsureCompletion();delete MyTask;
}
AsyncTask系统
AsyncTask系统是一套基于线程池的异步任务处理系统,样例如下:
//AsyncWork.hclass ExampleAsyncTask : public FNonAbandonableTask{friend class FAsyncTask<ExampleAsyncTask>;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 jobFAsyncTask<ExampleAsyncTask>* MyTask = new FAsyncTask<ExampleAsyncTask>( 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线程池
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 threadif (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 purposesGLargeThreadPool = 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 dobContinueWaiting = !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下的事件机制,大家可以自行到网上搜索相关的使用,这里给出一个官方的 使用案例。
Asyntask与IQueuedWork
线程池的任务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提供了以下几个不同类别的同步机制:
Atomics 原子机制
Atomic operations(原子操作) 保证CPU在读取和写入内存时总线操作是不可分割的。它是许多高级同步机制的基础,主要优势是可以进行比较快的进行比较和解锁操作。一个用Atomics实现的样例如下:
class FThreadSafeCounter{
public:
int32 Add( int32 Amount ) {return FPlatformAtomics::InterlockedAdd(&Counter, Amount);}
private:volatile int32 Counter; // 因为值可能以编译器无法预测的异步方式被改变,声明为volatile禁用优化
};
Locking 锁机制
在UE4中常用的两种锁机制是 Critical Sections(临界区)和 SpinLocks(自旋锁)。
- FSpinLock 自旋锁
- FScopeLock区域锁
- FCriticalSection 临界区
- FRWLock 读写锁
Signaling 信号机制
- FSemaphore信号量与互斥锁类型,但是他包含了一种信号机制。缺点是不是所有平台都支持。更加常用的线程间通信机制是 FEvent。
Waiting
- FEvent事件
- 阻塞直至被触发或者超时
- 经常被用来激活其他工作线程
- FScopedEvent区域事件
- 对FEvent的一次包装,阻塞在域代码退出时
{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】通常都不是线程安全的,需要我们仔细编写代码保证线程安全。下面是几个常见的线程安全类:
- FThreadSafeCounter计数器
- FThreadSingleton 单例类
- FThreadIdleStats 线程空闲状态统计类
- TLockFreePointerList 无锁队列
- TQueue队列
下面是一个简单的线程安全TSet,附带FCriticalSection使用示例。
/** Simple thread safe proxy for TSet<FName> */
template <typename T>
class FThreadSafeSet
{TSet<T> 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<T>& 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<ExampleAsyncTask>;friend class FAutoDeleteAsyncTask<ExampleAsyncTask>;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<ExampleAsyncTask>* MyTask = new FAsyncTask<ExampleAsyncTask>(1);// 交由后台控制任务开始执行时机MyTask->StartBackgroundTask();// 确保线程被执行完成MyTask->EnsureCompletion();delete MyTask;// 2.2 线程池异步队列FAsyncTask<ExampleAsyncTask>* MyTask2 = new FAsyncTask<ExampleAsyncTask>(2);// 直接在当前线程中执行MyTask2->StartSynchronousTask();// 检查任务是否完成if (MyTask2->IsDone()) {UE_LOG(LogBlankProgram, Display, TEXT("MyTask2 is Done."));}MyTask2->EnsureCompletion();delete MyTask2;// 2.3 带自动销毁的异步任务// 交由后台控制任务开始执行时机(new FAutoDeleteAsyncTask<ExampleAsyncTask>(3))->StartBackgroundTask();// 直接在当前线程中开始执行(new FAutoDeleteAsyncTask<ExampleAsyncTask>(4))->StartSynchronousTask();return 0;
}