专题的第二篇,我们聊聊UE4中的多线程的基础设施。UE4中最基础的模型就是FRunnable和FRunnableThread,FRunnable抽象出一个可以执行在线程上的对象,而FRunnableThread是平台无关的线程对象的抽象。后面的篇幅会详细讨论这些基础设施。
1. FRunnable
UE4为我们抽象FRunnable的概念,让我们指定在线程上运行的一段逻辑过程。该过程通常是一个较为耗时的操作,例如文件IO;或者是常态为空闲等待(Idle)的循环,随时等待新执行命令到来。
FRunnable为我们提供了四个重要的接口:
class CORE_API FRunnable
{
public:// ....virtual bool Init();virtual uint32 Run() = 0;virtual void Stop() {}virtual void Exit() {}
};
- Init是对FRunnable对象的初始化,它是由FRunnableThread在创建线程对象后,进入线程函数的时候立即被FRunnableThread调用的函数,并不能由用户自己调用;
- Run是Runnable过程的入口,同样也是有FRunnableThread在Init成功后调用;
- Exit是Run正常退出后,由FRunnableThread调用,进行对FRunnable对象的清理工作;
- Stop是给用户使用的接口,当我们觉得必要时停止FRunnable.
例如一个空闲等待的FRunnable的实现:
class MyRunnable : public FRunnable
{
public:MyRunnable(): RunningFlag(false), WorkEvent(FPlatformProcess::GetSynchEventFromPool()){}~MyRunnable(){FPlatformProcess::ReturnSynchEventToPool(WorkEvent);WorkEvent = nullptr;}bool Init() override{// ..if(!WorkEvent)return false;RunningFlag.Store(true);}void Run() override{while(RunningFlag.Load()){WorkEvent->Wait(MAX_uint32);if(!RunningFlag.Load())break;// ...}}void Stop() override{if(RunningFlag.Exchange(false))WorkEvent->Trigger();}void Exit() overrdie{// ...RunningFlag.Store(false);}void Notify(){WorkEvent->Trigger();}private:TAtomic<bool> RunningFlag;FEvent* WorkEvent;// ...
};
原子变量RunningFlag作为Runnable对象的运行状态的标记,所以Run函数的主体就是在RunningFlag为ture的情况无限循环。WorkEvent是其他线程上执行的任务与MyRunnable交互的事件对象,通过Notify接口,可以唤醒它继续执行。MyRunnable从Wait中醒来时,还会检查一次RunningFlag,有可能是唤醒它的是Stop接口发出的事件。而Stop的实现,会判断一下标识是否Runnable已经退出,而不用再次发出事件了。
2. FRunnableThread
FRunnable需要依附与一个FRunnableThread对象,才能被执行。例如,我们如果要执行第一节的空闲等待的Runnable:
auto* my_runnable = new MyRunnable{};auto* runnable_thread = FRunnableThread::Create(my_runnable, "IdleWait");
FRunnableThread是平台无关的线程对象的抽象,它驱动着FRunnable的初始化,执行和清理,并提供了管理线程对象生命周期,线程局部存储,亲缘性和优先级等接口。
class FRunnableThread
{// ....// Tls 索引static uint32 RunnableTlsSlot;public:// 获取Tls索引static uint32 GetTlsSlot();// 平台无关的创建线程对象接口static FRunnableThread* Create(class FRunnable* InRunnable,const TCHAR* ThreadName,uint32 InStackSize,EThreadPriority InThreadPri,uint64 InThreadAffinityMask);public:// 设置线程优先级virtual void SetThreadPriority( EThreadPriority NewPriority ) = 0;// 挂起线程virtual void Suspend( bool bShouldPause = true ) = 0;// 杀死线程virtual bool Kill( bool bShouldWait = true ) = 0;// 同步等待线程退出virtual void WaitForCompletion() = 0;protected:// 平台相关的线程对象初始化过程virtual bool CreateInternal(FRunnable* InRunnable, const TCHAR* InThreadName,uint32 InStackSize,EThreadPriority InThreadPri, uint64 InThreadAffinityMask) = 0;
};
UE4已经实现了各个平台的线程对象。Win平台使用的是系统Windows的Thread API. 而其他平台是基于pthread,不同平台实现上略有不同。通过编译选项包含平台相关的头文件,并通过FPlatformProcess类型的定义来选择相应平台的实现。参见FRunnableThread::Create函数:
FRunnableThread* FRunnableThread::Create(class FRunnable* InRunnable, const TCHAR* ThreadName,uint32 InStackSize,EThreadPriority InThreadPri, uint64 InThreadAffinityMask)
{// ....NewThread = FPlatformProcess::CreateRunnableThread();if (NewThread){if (NewThread->CreateInternal(...))// .....}// ....
}
线程对象的创建,需要指定一个FRunnable对象的实例。
FPlatformProcess::CreateRunnableThread就是简单地new一个平台相关的线程对象,而真正的初始化时在FRunnableThread::CreateInternal当中完成的。线程平台相关的API差异很大,UE4的封装尽可能地让各个平台的实现略有不同。
系统API创建的线程对象,都以_ThreadProc作为入口函数。接下来是一系列的平台相关的初始化工作,例如设置栈的大小,TLS的索引,亲缘性掩码,获取平台相关的线程ID等。之后,就会进入上一节我们提及的FRunnable的初始化流程中了。一个线程创建成功的时序图如下:
Win平台的实现中,由于API的历史原因需要_ThreadProc的调用约定是STDCALL. 因此Win平台下的_ThreadProc函数,是一个转发函数,转发给了另外一个CDECL调用约定的函数FRunnableThreadWin::GuardedRun.
3. Runnable or Callable
UE4的多线程模型是Runnable和Thread,但是有不少C++库,如标准库,是Callable and Thread. 如果使用标准库的std::thread:
int main(void)
{std::thread t{ [](){ std::cout << "Hello Thread." } };t.join();return 0;
}
暂时忽略标准库thread简陋的设施,Callable和Runnable这两个模型是可以等价的,也就是他们可以相互表达。
例如我们可以用UE4的设施,实现类似std::thread的FThread(UE4已经实现了一个):
class FThread final : public FRunnable
{
public:template <typename Func, typename ... Args>explicit FThread(Func&& f, Args&& ... args): Callable(create_callable(std::forward<Func>(f), std::forward<Args>(args)...)), Thread(FRunnableThread::Create(this, "whatever")){if(!Thread)throw std::runtime_error{ "Failed to create thread!" };}void join(){Thread->WaitForCompletion();}virtual uint32 Run() override{Callable();return 0;}private:template <typename Func, typename ... Args>static auto create_callable(Func&& f, Args&& ... args) noexcept{// 为了简单起见用了20的特性,如果是17标准以下的话,用tuple也能办到。// Eat return typereturn [func = std::forward<Func>(f), ... args = std::forward<Args>(args)](){std::invoke(func, std::forward<Args>(args...));};}private:TFunction<void()> Callable;FRunnableThread* Thread;
};
我们还可以用std::thread和一些封装,来实现一个的RunnableThread. 下面是一个简单的实现:
class RunnableThread
{
public:explicit RunnableThread(FRunnable* runnable): runnable_(runnable), inited_(false), init_result_(false), thread_(&RunnableThread::Run, this){std::unique_lock<std::mutex> lock{ mutex_ };cv_.wait(lock, [this](){ return inited_; });}protected:void Run(){auto result = runnable_->Init();{std::unique_lock<std::mutex> lock{ mutex_ };inited_ = true;init_result_ = result;}cv_.notify_one();if(result){runnable_->Run();runnable_->Exit();}}private:FRunnable* runnable_;bool inited_;bool init_result_;std::thread thread_;std::mutex mutex_;std::condition_variable cv_;
};
虽然笔者不喜欢面向对象的设计(OOD),但UE4的FRunnable和FRunnaableThread实现得确实挺不错。没有很重的框架束缚,并且FRunnable也有着跟callable一样的表达能力,并且FRunnableThread封装了各个平台线程库几乎所有的功能特性。总体上来说,比标准库的thread设施更齐全。
4. 小结
UE4中的多线程模型用一句话概括为: A FRunnable runs on a FRunnableThread.
FRunnable是逻辑上的可执行对象的概念的抽象。对于一个具体的可执行对象的实现,用户需要实现Init和Exit接口,对Runnable需要的系统资源进行申请和释放;用户需要实现Run来控制Runnable的执行流程,并在需要的情况下实现Stop接口,来控制Runnable的退出。
FRunnableThread是UE4提供的平台无关的线程对象的抽象,并提供了控制线程对象生命周期和状态的接口。UE4实现了常见所有平台的线程对象,实现细节对用户透明。
除此之外,本文还讨论了Runnable与Callable两种模型,并且它们具有相同的表达能力。
这个系列的下一篇,将会讨论FQueuedThreadPool. 它是由FRunnable及FRunnableThread组合实现的,用于执行任务队列的线程池。