一些关于UE的Thread的知识
注意
What Not to Do不该做什么
- 不要尝试在其他线程修改、创建或删除 UObject!
您可以准备所有数据/执行所有计算,但只有游戏线程应该实际生成/修改/删除 UObjects/AActors。
- Dont try to use TimerManager outside of the game thread 😃不要尝试在游戏线程之外使用 TimerManager 😃
- Don’t try to draw debug lines/points etc, as it will likely crash, ie DrawDebugLine(etc…)不要尝试绘制调试线/点等,因为它可能会崩溃,即 DrawDebugLine(etc…)
Notice (since 4.11):注意(自4.11起):
If you want to use the timer, remove, and modify variables use it:如果您想使用计时器、删除和修改变量,请使用它:
#include “Async.h” … AsyncTask(ENamedThreads::GameThread, { // code to execute on game thread here });#include“异步.h” … AsyncTask(ENamedThreads::GameThread, { // 在游戏线程上执行的代码 });
遇到的问题
另一个线程使用引用捕获失败
问题描述:UE中使用runnable创建出来的线程里使用AsyncTask(ENamedThreads::Type::GameThread,[Location,Rotation,this](){}可以捕获Location和Rotation但是如果使用引用捕获则不行,这是为什么
//当前线程:Runnable的线程
FVector Location = FVector(lon,lat,alt);
FRotator Rotation = FRotator(pitch,heading,roll);
AsyncTask(ENamedThreads::Type::GameThread,[/*&*/Location,/*&*/Rotation,this]()
{
UE_LOG(LogTemp,Warning,TEXT("%f"),Location.X);
if (!OnReceiveSyncTrans.ExecuteIfBound(Location, Rotation))
{
UE_LOG(LogTemp,Warning,TEXT("OnReceiveSyncTrans not execute"));
}
});
悬垂引用:若以引用隐式或显式捕获非引用实体,而在该实体的生存期结束之后调用lambda对象的函数调用运算符,则发生未定义行为。C++ 的闭包并不延长被捕获的引用的生存期。这同样适用于被捕获的this指针所指向的对象的生存期。因此,在使用this捕获的情况下,需要确保Lambda的执行发生在this指向的对象的有效生命周期内。
获取当前线程ID和名字
每个线程都有一个线程ID,线程ID在它所属的进程环境中有效。为增加标识性,UE4还增加了线程名称。线程ID是唯一的,线程名称可以重复。可通过GetThreadID和 GetThreadName 获取线程ID和名称。
uint32 CurrentThreadId = FPlatformTLS::GetCurrentThreadId();
FString CurrentThreadName = FThreadManager::Get().GetThreadName(CurrentThreadId);
FRunnable的使用
FRunnable是UE4中多线程的实现方式之一,适用于复杂运算。
FRunnable需要依附与一个FRunnableThread对象,才能被执行。
- FRunnableThread通过FRunnableThread::Create来调用对应平台的接口来创建线程,并调用 FRunnable 对象的Init,Run等事件。Create执行之后,对应的线程函数Run立即执行,并没有Start之类的方法。
- FRunnable定义了Init(),Run(),Exit()等虚函数,首先执行Init,初始化完成后执行Run,Run执行完毕之后执行Exit。 主动调用Stop,可以提前终止线程。
- 如果要结束线程,可以调用FRunnableThread的Kill方法。Kill方法仅仅调用了RunnableObject的Stop方法(既然你已经拿到了RunnableObject,就直接调用Stop方法即可),因此需要手动在Stop方法中将退出信息传递给Run函数,从而结束Run方法。
class FMyRunnable : public FRunnable
{
protected:
// FRunnable interface
virtual bool Init() override;// 初始化 runnable 对象,在FRunnableThread创建线程对象后调用
virtual uint32 Run() override;// Runnable 对象逻辑处理主体,在Init成功后调用
virtual void Stop() override;// 停止 runnable 对象, 线程提前终止时被用户调用 如:Kill()或直接调用Stop()
virtual void Exit() override;// 退出 runnable 对象,由FRunnableThread调用,在Run函数return后会调用此函数。
// ~FRunnable interface
private:
FRunnableThread* Thread;
FThreadSafeBool bRunning;
}
//D:\UE_5.4\Engine\Source\Runtime\Core\Private\Windows\WindowsRunnableThread.cpp 用Windows平台举例
uint32 FRunnableThreadWin::Run()
{
uint32 ExitCode = 1;
check(Runnable);
if (Runnable->Init() == true) //初始化 runnable 对象,在FRunnableThread创建线程对象后调用
{
ThreadInitSyncEvent->Trigger();
SetTls();
ExitCode = Runnable->Run(); // Runnable 对象逻辑处理主体,在Init成功后调用
Runnable->Exit(); // 退出 runnable 对象,由FRunnableThread调用
#if STATS
FThreadStats::Shutdown();
#endif
FreeTls();
}
else
{
ThreadInitSyncEvent->Trigger();
}
return ExitCode;
}
//D:\UE_5.4\Engine\Source\Runtime\Core\Private\Microsoft\MicrosoftRunnableThread.h
// Kill函数大致如下,会先调用Runnable的Stop函数,而且应当注意,如果Suspend(true)时调用Kill(false)会崩
virtual bool Kill(bool bShouldWait) override
{
// Let the runnable have a chance to stop without brute force killing
if (Runnable)
{
Runnable->Stop();
}
if (bShouldWait == true)
{
// Wait indefinitely for the thread to finish. IMPORTANT: It's not safe to just go and
// kill the thread with TerminateThread() as it could have a mutex lock that's shared
// with a thread that's continuing to run, which would cause that other thread to
// dead-lock.
//
// This can manifest itself in code as simple as the synchronization
// object that is used by our logging output classes
WaitForSingleObject(Thread, INFINITE);
}
CloseHandle(Thread);
Thread = NULL;
return bDidExitOK;
}
获取线程名字
FRunnable有自己的获取名字和ID的方法需要通过FRunnableThread调用
Thread->GetThreadName();
当然通过FThreadManager仍然是可以的,因为通过FRunnableThread 创建的线程是通过FThreadManager进行统一管理。
FThreadManager::Get().AddThread(ThisThread->GetThreadID(), ThisThread);
UE多线程源码剖析
GraphTask
GraphTask是Unreal Engine 4中的一个任务调度系统,用于并行执行任务和管理任务之间的依赖关系,Graph Task可以将任务分配给多个线程并行执行,以充分利用多核处理器的计算能力,并且可以管理任务之间的依赖关系,确保任务按照正确的顺序执行。依赖关系主要通过设置Prerequisites来实现。GraphTask可以很方便的实现异步和基于优先级执行。是一种对任务很方便管理的类。

依赖关系的实现并不复杂,如上主要是计算Prerequisites列表所以得Graph队列是否全部执行完毕,全部执行完再运行执行该Graph。通过使用TGraphTask,开发者可以将游戏中的复杂计算和任务分解为多个。
FTaskGraphImplementation
FTaskGraphImplementation是真正为TGraphTask服务和管理的类,负责创建和管理任务图,其中包含了多个任务节点和它们之间的依赖关系。它会根据任务之间的依赖关系和优先级来确定任务的执行顺序,并将任务分配给可用的线程进行并行执行。它会根据系统的硬件配置和任务的负载情况来动态调整线程的数量,以充分利用多核处理器的计算能力。
FAsyncTask
FAsyncTask是Unreal Engine 4中的一个异步任务管理类,可以将耗时的操作设置为异步执行,可以设置任务完成后的回调函数,可以取消正在执行的任务可以为任务设置优先级,以确保重要的任务优先执行。FAsyncTask适用于需要在后台执行的独立任务,如文件加载、网络请求、复杂计算等。它可以在任务执行完毕后进行相应的处理,如更新游戏状态、刷新UI等。而TGraphTask适用于需要并行处理的任务集合,处理依赖关系保证顺序等等,如复杂的计算、渲染,动画,物理数据更新等。
FRunnable
FRunnable是UE更底层的控制线程机制的类,提供了一些方法来控制任务的执行,如启动线程、暂停线程、恢复线程和停止线程等,也提供了一些线程同步的机制,如互斥锁(Mutex)和信号量(Semaphore),用于保护共享资源的访问,避免多线程竞争导致的数据不一致问题。FRunnable定义了线程的生命周期,包括线程的创建、启动、执行和结束等。开发者可以根据需要实现相应的方法来管理线程的生命周期。总之这是一个更底层的控制线程的类方法。
FRunnableThread 可以创建和管理后台线程,用于执行FRunnable中定义的任务。FRunnableThread是一个线程管理类,用于创建和管理后台线程。它提供了一种方便的方式来启动、暂停、恢复和停止线程。而FRunnable是一个接口,用于定义可运行的线程类。它定义了线程的执行逻辑和任务,并提供了线程同步和生命周期管理的功能。开发者可以通过继承FRunnable接口来实现自定义的可运行线程类,并在FRunnableThread中创建和管理该线程。FRunnableThread会调用FRunnable的方法来执行任务,并提供线程同步和生命周期管理的支持。简单说来就是一个是线程管理器,一个是线程类本身。

FQueuedThreadPool
FQueuedThreadPool是UE4的线程池,用于管理和调度多个线程执行任务。线程池内有大量的QueueWork来管理线程,包括待执行线程,正在执行的线程和所有线程。在UE中有IO线程池GIOThreadPool ,后台线程池GBackgroundPriorityThreadPool ,以及游戏线程池GThreadPool。线程池主要是给FAsyncTask使用的,也就是这类的异步Task全部是通过线程池来执行任务的。
ParallelForImpl
ParallelForImpl并行计算用于实现并行循环执行任务的功能。它可以将一个任务分割成多个子任务,并在多个线程上并行执行这些子任务,以提高执行效率和性能。提供了一种方便的方式来控制循环的迭代次数和索引范围。ParallelFor会自动处理线程同步和任务分配的细节,确保子任务在不同线程上正确地执行,并在所有子任务完成后进行合并。这样可以避免多线程竞争导致的数据不一致问题。。它通过任务分割和线程同步来实现任务的并行执行,并根据系统的硬件配置和任务的负载情况进行性能优化。我们可以使用ParallelForImpl来自定义并行循环执行任务的逻辑,以满足特定的需求和优化要求。

FTaskGraphImplementation
FTaskGraphImplementation是真正为TGraphTask服务和管理的类,负责创建和管理任务图,其中包含了多个任务节点和它们之间的依赖关系。它会根据任务之间的依赖关系和优先级来确定任务的执行顺序,并将任务分配给可用的线程进行并行执行。它会根据系统的硬件配置和任务的负载情况来动态调整线程的数量,以充分利用多核处理器的计算能力。

几大线程相关类关系
如上图,展示了几个线程相关类的相互关系。在FTaskGraphImplementation中还管理了游戏主要线程的创建和销毁,主要工作线程包括WorkerThreads工作线程和ENamedThreads。所有的线程数据都存储在FWorkerThread WorkerThreads[MAX_THREADS];内部。前N个是NamedThread,后边是WorkerThread.
多线程教程链接:【UE·引擎篇】Runnable、TaskGraph、AsyncTask、Async多线程开发指南
参考链接
1,Multi-Threading: How to Create Threads in UE4
2,UE 多线程