為了提升游戲的運行幀率,減少卡頓,UE4中使用了大量的線程來提升游戲的並發程度,來釋放GamePlay游戲線程的壓力。
具體包括:
① 將渲染的應用程序階段的工作放在RenderThread中
② 將渲染命令提交放在RHIThread中
③ 將Actor及ActorComponent的Tick、GC Mark等放到TaskGraph中並行化
④ GC Sweep的內存釋放邏輯放在FAsyncPurge線程中
⑤ 資源放到AsyncLoading線程中異步加載
⑥ 物理模擬會在PhysX內部的線程中計算
⑦ 聲音放在AudioThread線程中
⑧ Stat統計數據的收集放到StatThread線程中
⑨ 在FAsyncWriter線程中寫log到文件
#include "CoreMinimal.h" DEFINE_LOG_CATEGORY_STATIC(TestLog, Log, All); IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMultiThreadTest, "MyTest1.PublicTest.MultiThreadTest", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) // 可被Automation System識別 class FTest1Runable : public FRunnable { public: FTest1Runable(int32 index) { ThreadIndex = index; UE_LOG(TestLog, Log, TEXT("FTest1Runable Contruct: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter); } virtual ~FTest1Runable() override { UE_LOG(TestLog, Log, TEXT("FTest1Runable Destruct: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter); } virtual bool Init() override // 線程創建后,執行初始化工作 【在線程上執行】 { UE_LOG(TestLog, Log, TEXT("FTest1Runable Init: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter); return true; } virtual uint32 Run() override // 放置線程運行的代碼 【在線程上執行】 { UE_LOG(TestLog, Log, TEXT("FTest1Runable Run: 111 ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter); // FPlatformProcess::Sleep(xx)可讓當前線程Suspend xx秒 注:其他線程不受影響 switch (ThreadIndex) { case 0: FPlatformProcess::Sleep(20.0f); default: FPlatformProcess::Sleep(1.0f); break; } UE_LOG(TestLog, Log, TEXT("FTest1Runable Run: 222 ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter); FPlatformProcess::Sleep(2.0f); UE_LOG(TestLog, Log, TEXT("FTest1Runable Run: 333 ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter); return 0; } virtual void Stop() override { } virtual void Exit() override // 線程退出之前,進行清理工作 【在線程上執行】 { UE_LOG(TestLog, Log, TEXT("FTest1Runable Exit: ThreadIndex:%d tid:0x%x FrameIndex:%llu"), ThreadIndex, FPlatformTLS::GetCurrentThreadId(), GFrameCounter); } private: int32 ThreadIndex; }; bool FMultiThreadTest::RunTest(const FString& Parameters) { UE_LOG(TestLog, Log, TEXT("RunTest Begin tid:0x%x FrameIndex:%llu"), FPlatformTLS::GetCurrentThreadId(), GFrameCounter); // 調用FRunnableThread::Create來創建一個線程 FRunnableThread* Test1Thread0 = FRunnableThread::Create(new FTest1Runable(0), TEXT("Test1Thread0")); // Test1Thread0為線程名 FRunnableThread* Test1Thread1 = FRunnableThread::Create(new FTest1Runable(1), TEXT("Test1Thread1")); FRunnableThread* Test1Thread2 = FRunnableThread::Create(new FTest1Runable(2), TEXT("Test1Thread2")); UE_LOG(TestLog, Log, TEXT("RunTest End tid:0x%x FrameIndex:%llu"), FPlatformTLS::GetCurrentThreadId(), GFrameCounter); return true; }
Automation System(自動化測試系統) 菜單:Window -- Developer Tools -- Session Frontend
執行流程解釋如下:
// 游戲線程 ID:0xe08 Test1Thread0 ID: 0xa67c Test1Thread1 ID: 0x7104 Test1Thread2 ID: 0xb294 // 在第6390幀 游戲線程調用FRunnableThread::Create創造出Test1Thread0、Test1Thread1、Test1Thread2 // 各個線程被創建出來后,在自己的線程中立即調用了Init和Run方法 [2020.09.28-07.11.59:411][390]TestLog: RunTest Begin tid:0xe08 FrameIndex:6390 [2020.09.28-07.11.59:411][390]TestLog: FTest1Runable Contruct: ThreadIndex:0 tid:0xe08 FrameIndex:6390 [2020.09.28-07.11.59:412][390]TestLog: FTest1Runable Init: ThreadIndex:0 tid:0xa67c FrameIndex:6390 [2020.09.28-07.11.59:412][390]TestLog: FTest1Runable Run: 111 ThreadIndex:0 tid:0xa67c FrameIndex:6390 [2020.09.28-07.11.59:412][390]TestLog: FTest1Runable Contruct: ThreadIndex:1 tid:0xe08 FrameIndex:6390 [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Init: ThreadIndex:1 tid:0x7104 FrameIndex:6390 [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Run: 111 ThreadIndex:1 tid:0x7104 FrameIndex:6390 [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Contruct: ThreadIndex:2 tid:0xe08 FrameIndex:6390 [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Init: ThreadIndex:2 tid:0xb294 FrameIndex:6390 [2020.09.28-07.11.59:413][390]TestLog: FTest1Runable Run: 111 ThreadIndex:2 tid:0xb294 FrameIndex:6390 [2020.09.28-07.11.59:413][390]TestLog: RunTest End tid:0xe08 FrameIndex:6390 // 在第6474幀 // 過了1s,線程Test1Thread1、Test1Thread2執行到Run 222處 [2020.09.28-07.12.00:418][475]TestLog: FTest1Runable Run: 222 ThreadIndex:1 tid:0x7104 FrameIndex:6474 [2020.09.28-07.12.00:418][475]TestLog: FTest1Runable Run: 222 ThreadIndex:2 tid:0xb294 FrameIndex:6474 // 在第6690幀 // 再過了2s,線程Test1Thread1、Test1Thread2退出Run函數,並調用Exit函數,線程的生命周期結束 [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Run: 333 ThreadIndex:2 tid:0xb294 FrameIndex:6690 [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Exit: ThreadIndex:2 tid:0xb294 FrameIndex:6690 [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Run: 333 ThreadIndex:1 tid:0x7104 FrameIndex:6690 [2020.09.28-07.12.02:416][691]TestLog: FTest1Runable Exit: ThreadIndex:1 tid:0x7104 FrameIndex:6690 // 在第8832幀 // 過了10s,線程Test1Thread0執行到Run 222處 [2020.09.28-07.12.20:419][833]TestLog: FTest1Runable Run: 222 ThreadIndex:0 tid:0xa67c FrameIndex:8832 // 在第9072幀 // 再過了2s,線程Test1Thread0退出Run函數,並調用Exit函數,線程的生命周期結束 [2020.09.28-07.12.22:419][ 73]TestLog: FTest1Runable Run: 333 ThreadIndex:0 tid:0xa67c FrameIndex:9072 [2020.09.28-07.12.22:419][ 73]TestLog: FTest1Runable Exit: ThreadIndex:0 tid:0xa67c FrameIndex:9072
如果想在單線程運行模式下(帶上-nothreading參數),以假線程FFakeThread的方式在游戲線程上來模擬執行的話,需要從FSingleThreadRunnable派生,並實現其GetSingleThreadInterface接口中返回當前this指針
class FStatsThread : public FRunnable , private FSingleThreadRunnable { // ... ... public: virtual FSingleThreadRunnable* GetSingleThreadInterface() override { return this; } };
1. FRunnable是線程可執行實體,其Run()函數為線程函數。Run()函數執行完,線程的生命周期也將結束
2. FRunnableThread是所有線程的基類,FRunnable為其成員變量
3. FRunnableThread相當於一個“外殼”,根據平台會創建出屬於那個平台的線程。而FRunnable是“核”,定義了這個線程具體要做什么
4. FThreadManager是全局的線程管理單例(通過靜態函數FThreadManager::Get()得到單例),可獲取到當前運行的所有線程
5. FRunnableThreadWin用於windows平台,FRunnableThreadPThread(對pthread的封裝)用於Android、iOS、Mac、Linux等平台
6. FFakeThread用於單線程運行模式下(帶上-onethread參數),以假線程的方式在游戲線程上來模擬執行
7. FRunnableThread相關函數
const uint32 GetThreadID() const; // 線程ID,唯一
const FString & GetThreadName() const; // 線程名稱 可重復
EThreadPriority GetThreadPriority(); // 獲取線程的優先級
void SetThreadPriority( EThreadPriority NewPriority ); // 設置線程的優先級
void Suspend( bool bShouldPause = true ); // bShouldPause為true時,pause線程;bShouldPause為false時,resume線程
void WaitForCompletion(); // 阻塞並等待當前線程執行完畢
bool Kill( bool bShouldWait = true ); // 會先執行 runnable 對象的Stop函數,然后根據 bShouldWait 參數決定是否等待線程執行完畢。如果不等待,則強制殺死線程,可能會造成內存泄漏
線程 | 平台 | 解釋 |
主線程 |
All |
相關的代碼在:UnrealEngine\Engine\Source\Runtime\Launch目錄中
windows:為游戲線程 線程函數為:LaunchWindows.cpp下的WinMain函數 mac:為游戲線程 線程函數為:INT32_MAIN_INT32_ARGC_TCHAR_ARGV,其實展開就是main 內部會調用到obj c的NSApp(系統提供的App對象) 具體應用能實現的就只有后面的Delegate,所以UE4實現了UE4AppDelegate 真正做初始化在applicationDidFinishLaunching函數中,然后調用runGameThread函數 Android:線程名為MainThread-UE4 使用java代碼來處理主消息循環 Splash Activity:Engine\Build\Android\Java\src\com\epicgames\ue4\SplashActivity.java 游戲Activity:Engine\Build\Android\Java\src\com\epicgames\ue4\GameActivity.java.template iOS:線程名為Thread <n> 如:Thead 1 線程函數為LaunchIOS.cpp下的main函數,會調用到obj c的UIApplicationMain 具體實現在IOSAppDelegate.cpp的MainAppThread函數中
對於Android和iOS,GameThread並不是主線程。在接入第三方SDK,一般都會從app的主線程調用回調 這時如果直接調用UE4相關函數,很有可能發生不可預知的問題,可通過AsyncTask將這些操作放在GameThread中跑 AsyncTask(ENamedThreads::GameThread, [=]() { |
游戲線程(消耗高) |
All | Windows:線程名為Main Thread 線程函數為:LaunchWindows.cpp下的WinMain函數 Android:線程名為Thread-<n> 如:Thread-3 線程函數為LaunchAndroid.cpp下的android_main函數 iOS:線程名為Thread <n> 如:Thread 5 線程函數為IOSAppDelegate.cpp下的MainAppThread函數
線程id:uint32 GGameThreadId 會被加入到TaskGraph系統中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::GameThread)
bool IsInGameThread(); |
渲染線程(消耗高)
class FRenderingThread : public FRunnable 線程函數:RenderingThreadMain |
All | 線程名:RenderThread 1
-norenderthread參數可在啟動時強制不開啟RenderThread 注:開啟-norenderthread參數時,最好是帶上-NoLoadingScreen來禁掉loadingscreen,要不然會出現崩潰
可用ToggleRenderingThread控制台命令來創建和銷毀RenderThread 銷毀RenderThread時,渲染相關的邏輯會放回到游戲線程中
渲染線程的創建邏輯在void StartRenderingThread()函數中 銷毀邏輯在void StopRenderingThread()函數中
線程id:uint32 GRenderThreadId FRunnableThread* GRenderingThread; 會被加入到TaskGraph系統中:FTaskGraphInterface::Get().AttachToThread(RenderThread)
bool GUseThreadedRendering; // 是否使用獨立Render線程來渲染 bool GIsThreadedRendering; // 渲染是否在獨立的Render線程中運行 TAtomic<int32> GIsRenderingThreadSuspended; // 渲染線程是否暫停 TAtomic<int32> GSuspendRenderingTickables; // rendering tickables是否應該更新 flush時應暫停更新
bool IsInActualRenderingThread(); // 渲染線程存在,且當前線程為渲染線程 bool IsInRenderingThread(); // 無渲染線程||渲染線程暫停||當前線程為渲染線程 bool IsInParallelRenderingThread(); // 與IsInRenderingThread()函數等價 |
RHI線程
class FRHIThread : public FRunnable |
All | 使用獨立的渲染線程時,會根據情況來決定是否創建該線程 當渲染線程銷毀時,該線程也會銷毀,RHI執行的邏輯會放回游戲線程中
-rhithread參數時(缺省),由各個平台來決定啟動時是否開啟RHI線程 對應bool GRHISupportsRHIThread變量,具體情況如下: DX11缺省不開啟,可以通過#define EXPERIMENTAL_D3D11_RHITHREAD 1來缺省開啟 DX12在非Editor模式下默認開啟 OpenGL根據FeatureLevel和r.OpenGL.AllowRHIThread的值來決定是否缺省開啟 Vulklan缺省開啟1個RHI線程 Metal會根據顯卡芯片版本和r.Metal.IOSRHIThread的值來決定是否缺省開啟
IOS缺省是沒有開該線程的
-norhithread參數會在啟動時強制不開啟rhi線程 如果不開啟rhi線程,rhi的邏輯會跑在RenderingThread線程中 在pc環境的運行時,可用r.RHIThread.Enable 0控制台命令切到這種方式來跑
線程id:uint32 GRHIThreadId FRunnableThread* GRHIThread_InternalUseOnly 會被加入到TaskGraph系統中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::RHIThread)
// 創建獨立的RHIThread,放加入到TaskGraph中,RHI會跑在TaskGraph的RHIThread上 // 在pc環境的運行時,可用r.RHIThread.Enable 1控制台命令切到這種方式來跑 bool GUseRHIThread_InternalUseOnly; // 在pc環境的運行時,可用r.RHIThread.Enable 2控制台命令切到這種方式來跑 bool GUseRHITaskThreads_InternalUseOnly; // TaskGraph中使用Any Thread來跑
// GUseRHIThread_InternalUseOnly為true,且正在運行時,該值為true bool GIsRunningRHIInDedicatedThread_InternalUseOnly; --》bool IsRunningRHIInDedicatedThread() // GUseRHITaskThreads_InternalUseOnly為true,且正在運行時,該值為true bool GIsRunningRHIInTaskThread_InternalUseOnly; --》bool IsRunningRHIInTaskThread()
// GUseRHIThread_InternalUseOnly或GUseRHITaskThreads_InternalUseOnly為true,且正在運行時,該值為true bool GIsRunningRHIInSeparateThread_InternalUseOnly; --》bool IsRunningRHIInSeparateThread()
bool IsInRHIThread(); bool IsRHIThreadRunning(); // 存在RHI線程 |
RenderingThread心跳監視線程
class FRenderingThreadTickHeartbeat : public FRunnable |
All | 線程名:RTHeartBeat 1
負責執行rendering thread tickables
使用獨立的渲染線程時,才會創建該線程,來執行void TickRenderingTickables() 當渲染線程銷毀時,該線程也會銷毀,TickRenderingTickables會放回游戲線程中來執行
TAtomic<bool> GRunRenderingThreadHeartbeat // 是否使用RenderingThread心跳監視線程 // 當使用獨立線程來渲染時,會被設置成true;獨立渲染線程停止時,會被設置成false float GRenderingThreadMaxIdleTickFrequency = 40.f; // tick頻率 值越大tick次數越多,性能越差 |
線程池線程
class FQueuedThread : public FRunnable |
All | 線程名:PoolThread 0 ... PoolThread <n>
// Global thread pool for shared async operations #if WITH_EDITOR |
TaskGraph線程(消耗高)
class FTaskThreadAnyThread : public FTaskThreadBase 注:class FTaskThreadBase : public FRunnable, FSingleThreadRunnable |
All | 線程名:TaskGraphThreadHP 0 ... TaskGraphThreadHP <n> TaskGraphThreadNP 0 ... TaskGraphThreadNP <n> TaskGraphThreadBP 0 ... TaskGraphThreadBP <n> 進程優先級:HP > NP > BP
TaskGraph只在最開始構造函數中創建所有的線程,調用棧如下: UE4Editor-Core-Win64-Debug.dll!FTaskGraphImplementation::FTaskGraphImplementation(int __formal) Line 1158 |
AudioThread線程(消耗較高)
class FAudioThread : public FRunnable 線程函數:AudioThreadMain |
All | 線程名:AudioThread
會被加入到TaskGraph系統中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::AudioThread)
bool IsInAudioThread(); |
AudioMixerRenderThread線程(消耗高) class IAudioMixerPlatformInterface : public FRunnable, public FSingleThreadRunnable, public IAudioMixerDeviceChangedLister |
All | 線程名:AudioMixerRenderThread |
class FAsyncLoadingThread final : public FRunnable, public IAsyncPackageLoader |
All | 在EDL(EventDrivenLoader)模式下,是不開啟該獨立線程的,在主線程中Loading bool GEventDrivenLoaderEnabled為ture時為EDL模式
編輯器、standalone非cook版本為EDL模式,沒有該線程 Android、IOS等cook版本為Async模式,會開啟該線程
bool IsInAsyncLoadingThread(); --》bool IsInAsyncLoadingThreadCoreUObjectInternal() bool IsAsyncLoading(); --》bool IsAsyncLoadingCoreUObjectInternal() void SuspendAsyncLoading(); --》void SuspendAsyncLoadingInternal() void ResumeAsyncLoading(); --》void ResumeAsyncLoadingInternal() bool IsAsyncLoadingSuspended(); --》bool IsAsyncLoadingSuspendedInternal() bool IsAsyncLoadingMultithreaded(); --》bool IsAsyncLoadingMultithreadedCoreUObjectInternal() |
class FAsyncLoadingThread2 final : public FRunnable, public IAsyncPackageLoader | 暫時沒用 | |
class FAsyncLoadingThreadWorker : private FRunnable | 暫時沒用 | |
防屏保線程 class FScreenSaverInhibitor : public FRunnable |
桌面平台 宏PLATFORM_DESKTOP為1 Windows、Linux、Mac |
線程名:ScreenSaverInhibitor |
StatsThread線程(消耗高) class FStatsThread : public FRunnable, FSingleThreadRunnable |
All | 線程名:StatsThread
會被加入到TaskGraph系統中:FTaskGraphInterface::Get().AttachToThread(ENamedThreads::StatsThread) |
FMediaTicker線程 class FMediaTicker : public FRunnable , public IMediaTicker |
All | 線程名:FMediaTicker |
class FAsyncPurge : public FRunnable |
All | extern int32 GMultithreadedDestructionEnabled;
[/Script/Engine.GarbageCollectionSettings] gc.MultithreadedDestructionEnabled=True // 通過這個控制台命令來開啟
多線程GC清理 |
|
All | 線程名:SlateLoadingThread1
LoadingScreen播放視頻或Slate UI
bool IsInSlateThread(); |
class FPreLoadScreenSlateThreadTask : public FRunnable | All | 線程名:SlateLoadingThread1
引擎初始化播放視頻
bool IsInSlateThread(); |
template<typename ResultType> |
All | TAsync 0 |
class FAsyncWriter : public FRunnable, public FArchive | All | FAsyncWriter_UAGame
見:OutputDeviceFile.cpp的FAsyncWriter::Run() 異步寫log到文件 |
class FOnlineAsyncTaskManager : public FRunnable, FSingleThreadRunnable | All | OnlineAsyncTaskThreadNull DefaultInstance(1) |
class FLwsWebSocketsManager: public IWebSocketsManager, public FRunnable, public FSingleThreadRunnable | All | LibwebsocketsThread |
class FHttpThread : FRunnable, FSingleThreadRunnable | All | HttpManagerThread |
class FMessageRouter : public FRunnable, private FSingleThreadRunnable | All | FMessageBus.DefaultBus.Router |
class FLiveLinkMessageBusDiscoveryManager : FRunnable | All | LiveLinkMessageBusDiscoveryManager |
class FFileTransferRunnable : public FRunnable | All | FFileTransferRunnable |
class TcpConsoleListener : FRunnable | iOS | |
class FTcpListener : public FRunnable | iOS | |
@implementation FIOSFramePacer -(void)run:(id)param |
iOS | |
windows平台Splash線程 | windows | 線程函數:StartSplashScreenThread |
TraceLog | windows | windows:線程名為TraceLog;WindowsTrace.cpp下ThreadCreate函數 Android:線程名為bundle id;AndroidTrace.cpp下ThreadCreate函數 IOS:線程名為Thread <n>;AppleTrace.cpp下ThreadCreate函數 |
windows平台崩潰監視線程 | windows | 線程函數:CrashReportingThreadProc |
Shader編譯線程 class FShaderCompileThreadRunnableBase : public FRunnable |
編輯器 | 線程名:ShaderCompilingThread 拉起ShaderCompileWorker.exe進程進行shader編譯 |
DistanceField構建線程 class FBuildDistanceFieldThreadRunnable : public FRunnable |
編輯器 | |
class FAssetDataDiscovery : public FRunnable |
編輯器 | 用於發現文件 線程名:FAssetDataDiscovery |
class FAssetDataGatherer : public FRunnable |
編輯器 | 從FAssetRegistry文件列表中搜集Asset數據 線程名:FAssetDataGatherer |
class FVirtualTextureDCCCacheCleanup final : public FRunnable |
編輯器 | 線程名:FVirtualTextureDCCCacheCleanup |
class FDDCCleanup : public FRunnable | 編輯器 | 線程名:FDDCCleanup |
Wwise編輯器連接線程 class FAkWaapiClientConnectionHandler : public FRunnable |
編輯器或windows、mac下的非shipping版本 |
線程名:WAAPIClientConnectionThread1 |
在Android局內時抓的一個ue4stats文件中,里面統計的線程如下:
參考