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() {} };例如一個空閑等待的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組合實現的,用于執行任務隊列的線程池。
總結
- 上一篇: php考勤分析,php考勤系统
- 下一篇: java ldap 分页_具有从属引用的