UE4多线程概述
为了提升游戏的运行帧率,减少卡顿,UE4中使用了大量的线程来提升游戏的并发程度,来释放GamePlay游戏线程的压力。
具体包括:
① 将渲染的应用程序阶段的工作放在RenderThread中
② 将渲染命令提交放在RHIThread中
③ 将Actor及ActorComponent的Tick、物理模拟、动画、GC Mark等放到TaskGraph中并行化
④ GC Sweep的内存释放逻辑放在FAsyncPurge线程中
⑤ 资源放到AsyncLoading线程中异步加载
⑥ 声音放在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线程 注:FRunnableThreadPThread该函数为空实现
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控制台命令切到这种方式来跑 注:RHI线程还在,只是不再往里面派发任务了
线程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的Tick驱动线程
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();
/** UnrealEngine\Engine\Source\Runtime\SlateCore\Private\Rendering\SlateRenderer.cpp **/ bool IsThreadSafeForSlateRendering() bool DoesThreadOwnSlateRendering() return false; |
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文件中,里面统计的线程如下:
参考