【UE4源代码观察】观察UE4的线程使用

0.起点

我想观察UE4编辑器运行时后台都存在哪些线程,以及他们都是怎么被创建的。
为此,我在出现项目浏览界面时,在Tick中打了一个断点,准备观察VS中显示的线程。
在这里插入图片描述
需要注意的是:
1.这个观察不是稳定的,下次打开很可能会有差别(但应有基础不变的东西)
2.此时还在项目浏览器界面,当真正进入编辑器后,线程应该会更多。

1.清点出现的线程

发现此刻有110个线程:
在这里插入图片描述
根据位置这一分栏的信息可以将他们分为两类:
第一类是能显示出具体运行位置的,如上图中众多开头为TaskGraphThreadPoolThread的线程都在Wait中。之所以能看到位置是因为这是自己编译的引擎,有pdb文件,而运行的位置就在UE4引擎代码中。
第二类是无法显示位置的,如下图中的ntdll.dll 线程Nvwgf2umx.dll 线程的运行位置都无法看到。看来他们并不运行在UE4引擎代码中。
在这里插入图片描述

1.1 清点UE4代码中的线程

清点下UE4代码中的线程(即上述分类中第一类的线程),总共70个:

主线程
UE4Editor-TraceLog.dll!<lambda_1d8e489c48ee42d54681200bbe1d4212>::<lambda_invoker_cdecl>
UE4Editor-Core.dll!FCrashReportingThread::CrashReportingThreadProc
TaskGraphThreadNP 0
TaskGraphThreadNP 1
TaskGraphThreadNP 2
TaskGraphThreadNP 3
TaskGraphThreadNP 4
TaskGraphThreadNP 5
TaskGraphThreadNP 6
TaskGraphThreadHP 7
TaskGraphThreadHP 8
TaskGraphThreadHP 9
TaskGraphThreadHP 10
TaskGraphThreadHP 11
TaskGraphThreadHP 12
TaskGraphThreadHP 13
TaskGraphThreadBP 14
TaskGraphThreadBP 15
TaskGraphThreadBP 16
TaskGraphThreadBP 17
TaskGraphThreadBP 18
TaskGraphThreadBP 19
TaskGraphThreadBP 20
StatsThread
PoolThread 0
PoolThread 1
PoolThread 2
PoolThread 3
PoolThread 4
PoolThread 5
PoolThread 6
PoolThread 7
PoolThread 8
PoolThread 9
PoolThread 10
PoolThread 11
PoolThread 12
PoolThread 13
PoolThread 14
PoolThread 15
PoolThread 16
PoolThread 17
PoolThread 18
PoolThread 19
PoolThread 20
PoolThread 21
PoolThread 22
PoolThread 23
PoolThread 24
FAsyncWriter_UE4
FMediaTicker
HttpManagerThread
LibwebsocketsThread
OnlineAsyncTaskThreadNull DefaultInstance(1)
FMessageBus.DefaultBus.Router
ShaderCompilingThread
FDDCCleanup
FAssetDataDiscovery
FAssetDataGatherer
FTcpMessageTransport
FUdpMessageProcessor
FUdpMessageBeacon
FUdpMessageProcessor.Sender
FFileTransferRunnable
AudioMixerRenderThread(1)
FAndroidDeviceDetectionRunnable
FAndroidDeviceDetectionRunnable
RenderThread 1
RTHeartBeat 1

观察到其中有大量的以TaskGraphThreadPoolThread开头的线程,可以确定是UE4自己的两套线程系统,适用于不同的场合。但是除此之外,还有很多有特定名字的线程,他们是被单独创建出来的。

1.2 清点非UE4代码中的线程

清点下非UE4代码中的线程(即上述分类中第二类的线程),总共40个:

ntdll.dll 线程
ntdll.dll 线程
ntdll.dll 线程
SogouPY.ime 线程
SogouPY.ime 线程
SogouPY.ime 线程
SogouPY.ime 线程
SogouPY.ime 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
nvwgf2umx.dll 线程
UdpMessageMulticastReceiver
UdpMessageUnicastReceiver
XAudio2_7.dll 线程
combase.dll 线程
NVFBCAsyncThread
mswsock.dll 线程

我从wikidll上查了几个dll的大概相关的内容:
ntdll.dll
是重要的Windows NT内核级文件
Nvwgf2umx.dll
NVIDIA 开发的,和显卡驱动有关。
Mswsock.dll
微软开发的,和Socket有关
combase.dll
微软开发的,和windows平台的COM(Component Object Model ) 有关

2.观察何时被创建

2.1 线程的名字

我观察到,UE4内的线程绝大多数都有个干净的名字,而非UE4内的线程都只是显示了一个dll的名字。因此我想UE4应该对线程的名字做了处理,调查后发现果然。
首先,尽管封装不同,但是最底层他们都调用了FRunnableThread::Create函数:

/**
 * Factory method to create a thread with the specified stack size and thread priority.
 *
 * @param InRunnable The runnable object to execute
 * @param ThreadName Name of the thread
 * @param InStackSize The size of the stack to create. 0 means use the current thread's stack size
 * @param InThreadPri Tells the thread whether it needs to adjust its priority or not. Defaults to normal priority
 * @return The newly created thread or nullptr if it failed
 */
static FRunnableThread* Create(
	class FRunnable* InRunnable,
	const TCHAR* ThreadName,
	uint32 InStackSize = 0,
	EThreadPriority InThreadPri = TPri_Normal,
	uint64 InThreadAffinityMask = FPlatformAffinity::GetNoAffinityMask() );

其中第二个参数ThreadName是VS调试时看到的线程的名字。
依照这个线索,可以对FRunnableThread::Create字符进行搜索,能观察到源代码中何处会创建线程:
在这里插入图片描述

2.2 TaskGraph的线程

TaskGraph中的线程在它的构造函数中,由FRunnableThread::Create创建的,不过线程的名字在之前通过一些逻辑进行了计算。细节见\Source\Runtime\Core\Private\Async\TaskGraph.cpp

WorkerThreads[ThreadIndex].RunnableThread = FRunnableThread::Create(&Thread(ThreadIndex), *Name, StackSize, ThreadPri, Affinity); 
2.3 ThreadPool的线程

与TaskGraph的一个不同之处是,TaskGraph只在最开始构造函数中创建了所有的线程,但ThreadPool的线程并不是在一开始就全创建完(我调试时出现了三次)
FRunnableThread::CreateFQueuedThread::Create函数中被调用:

/**
 * Creates the thread with the specified stack size and creates the various
 * events to be able to communicate with it.
 *
 * @param InPool The thread pool interface used to place this thread back into the pool of available threads when its work is done
 * @param InStackSize The size of the stack to create. 0 means use the current thread's stack size
 * @param ThreadPriority priority of new thread
 * @return True if the thread and all of its initialization was successful, false otherwise
 */
virtual bool Create(class FQueuedThreadPool* InPool,uint32 InStackSize = 0,EThreadPriority ThreadPriority=TPri_Normal)
{
	static int32 PoolThreadIndex = 0;
	const FString PoolThreadName = FString::Printf( TEXT( "PoolThread %d" ), PoolThreadIndex );
	PoolThreadIndex++;

	OwningThreadPool = InPool;
	DoWorkEvent = FPlatformProcess::GetSynchEventFromPool();
	Thread = FRunnableThread::Create(this, *PoolThreadName, InStackSize, ThreadPriority, FPlatformAffinity::GetPoolThreadMask());
	check(Thread);
	return true;
}

而上面的函数在FQueuedThreadPoolBase::Create中被调用:

for (uint32 Count = 0; Count < InNumQueuedThreads && bWasSuccessful == true; Count++)
	{
		// Create a new queued thread
		FQueuedThread* pThread = new FQueuedThread();
		// Now create the thread and add it if ok
		if (pThread->Create(this,StackSize,ThreadPriority) == true)
		{
			QueuedThreads.Add(pThread);
			AllThreads.Add(pThread);
		}
		else
		{
			// Failed to fully create so clean up
			bWasSuccessful = false;
			delete pThread;
		}
	}

FQueuedThreadPoolBase继承自FQueuedThreadPool,后者是个抽象类,其关于Create函数的定义如下:

/**
* Creates the thread pool with the specified number of threads
 *
 * @param InNumQueuedThreads Specifies the number of threads to use in the pool
 * @param StackSize The size of stack the threads in the pool need (32K default)
 * @param ThreadPriority priority of new pool thread
 * @return Whether the pool creation was successful or not
 */
virtual bool Create( uint32 InNumQueuedThreads, uint32 StackSize = (32 * 1024), EThreadPriority ThreadPriority=TPri_Normal ) = 0;

相关的代码都可以在Core模块找到

2.4 单独创建的线程

RenderThread 1RTHeartBeat 1都是单独创建的线程:
\Source\Runtime\RenderCore\Private\RenderingThread.cpp中可以找到创建的代码:

GRenderingThread = FRunnableThread::Create(GRenderingThreadRunnable, *BuildRenderingThreadName(ThreadCount), 0, FPlatformAffinity::GetRenderingThreadPriority(), FPlatformAffinity::GetRenderingThreadMask());
GRenderingThreadHeartbeat = FRunnableThread::Create(GRenderingThreadRunnableHeartbeat, *FString::Printf(TEXT("RTHeartBeat %d"), ThreadCount), 16 * 1024, TPri_AboveNormal, FPlatformAffinity::GetRTHeartBeatMask());
2.5 UE4中的非FRunnableThread创建的线程

虽然上面都在讨论使用FRunnableThread::Create创建的线程,但是还有少量的线程并不是由它创建,比如清点时发现的:

UE4Editor-TraceLog.dll!<lambda_1d8e489c48ee42d54681200bbe1d4212>::<lambda_invoker_cdecl>
UE4Editor-Core.dll!FCrashReportingThread::CrashReportingThreadProc

他们的名字并不干净,不像是FRunnableThread::Create创建的线程。
此外,在之前的博客【UE4源代码观察】尝试生成启动画面中也发现,启动画面窗口的这个线程并没有使用FRunnableThread::Create
我想他们不使用的原因应该是他们本身较为简单,功能上较为边缘化,不想依赖FRunnable。又或者是他们创建较早,FRunnable所依赖的内容还没有被初始化好。

3. FRunnable 与 FRunnableThread

FRunnable类根据功能被继承,有很多子类。它本身是一个抽象类

class CORE_API FRunnable
{
public:

	/**
	 * Initializes the runnable object.
	 *
	 * This method is called in the context of the thread object that aggregates this, not the
	 * thread that passes this runnable to a new thread.
	 *
	 * @return True if initialization was successful, false otherwise
	 * @see Run, Stop, Exit
	 */
	virtual bool Init()
	{
		return true;
	}

	/**
	 * Runs the runnable object.
	 *
	 * This is where all per object thread work is done. This is only called if the initialization was successful.
	 *
	 * @return The exit code of the runnable object
	 * @see Init, Stop, Exit
	 */
	virtual uint32 Run() = 0;

	/**
	 * Stops the runnable object.
	 *
	 * This is called if a thread is requested to terminate early.
	 * @see Init, Run, Exit
	 */
	virtual void Stop() { }

	/**
	 * Exits the runnable object.
	 *
	 * Called in the context of the aggregating thread to perform any cleanup.
	 * @see Init, Run, Stop
	 */
	virtual void Exit() { }

	/**
	 * Gets single thread interface pointer used for ticking this runnable when multi-threading is disabled.
	 * If the interface is not implemented, this runnable will not be ticked when FPlatformProcess::SupportsMultithreading() is false.
	 *
	 * @return Pointer to the single thread interface or nullptr if not implemented.
	 */
	virtual class FSingleThreadRunnable* GetSingleThreadInterface( )
	{
		return nullptr;
	}

	/** Virtual destructor */
	virtual ~FRunnable() { }
};

FRunnableThread根据平台被继承,比如他的子类有FRunnableThreadWin FRunnableThreadAndroid FRunnableThreadUnix
它拥有一个FRunnable* Runnable成员,而且在创建时也需要给他一个FRunnable
我想,FRunnableThread相当于一个“外壳”,根据平台会创建出属于那个平台的线程,而FRunnable是“核”,定义了这个线程具体要做什么。

4.一些资料:

官方Wiki:Multi-Threading:_How_to_Create_Threads_in_UE4是一个实践。概括讲,它继承了一个FRunnable并用FRunnableThread::Create让它跑起来。
官方Wiki:Multi-Threading:_Task_Graph_System是一个实践,概括讲,它用TaskGraph系统完成了上篇相同的事。
《Exploring in UE4》多线程机制详解[原理分析] - 知乎是一篇对UE4多线程系统的分析。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值