UE4|UE4/UE5 多线程开发 附件插件下载地址

原创文章,转载请注明出处。

虚幻引擎 多线程开发介绍

  • 前言
  • 封装的插件下载地址:
  • UE4有线程池了,为什么我还要封装一个线程池?
  • 1>FRunnable
  • 2>TGraphTask
  • 3>FAsyncTask
  • 4.我的插件
    • 4.1新建线程池的->同异步调用方式
    • 4.2 封装UE4提供的线程池的->同异步调用方式
    • 4.3 EventGraph使用宏
    • 4.4 资源同异步加载 还没测试好,不太好用,还在改进,插件持续改进过程中
  • 5.几点注意项
  • 6.需要做的思考

前言 这几天在做一些资源的Load。
测试阶段数据加载几分钟进程一直是卡死的状态,所以想用一下多线程。
主要是考虑到复用性和使用简单性质。不用写一坨坨的代码,所以用了四天时间做了一个多线程的封装插件,周末没回河北,都交代给这个插件了。
功能有:
1>封装了FRunnable,做了一个我们自己的线程池。
2>封装了FTaskGraph,做了管理类和宏封装。
3>同样封装了FNonAbandonableTask,也就是用FAsyncTask来用UE4线程池做事情。
4>封装了一下FStreamableManager。
封装的插件下载地址:
2021-09-29 14:28:19 插件有更新
改进项1:提效了线程池。
改进项2:将线程池增加了一个方法。具体逻辑为 清除线程池中队列未执行的任务,阻塞主线程,并等待正在执行的线程逻辑,等待执行完后打开主线程。调用StopThreadLogic()即可达到。
插件已经重新上传,2021-10-29 11:20:08。
下载地址
插件的介绍在下面 第四节
资源同异步加载 还没测试好,不太好用,还在改进,插件持续改进过程中
UE4有线程池了,为什么我还要封装一个线程池? 我是这么考虑的,UE4的AsyncTask内部也有一个线程池+队列的概念,FQueuedThreadPool。
一是 Engine其实挺多地方再调用它内置的线程池的,当然不是排斥,我们也能用;
主要是还第二点考虑 :UE4也提供了创建一个新的线程的方法,也就是FRunnable,如果我们调用了FRunnable去创建一个新的线程的话,就是用,那么用完之后呢?直接线程退出了,线程接着被清理掉了。创建和释放线程是有消耗的。
关于创建线程的消耗: 有人也做过一些测试
既然有消耗,也有高效对其复用的方案,线程池。这个就是我为什么要封装一个线程池的思考。
先说一点UE4的几种线程基础知识如下:
1>FRunnable
1>创建一个新的独立线程。 2>写法一般是将其做为基类,继承一次。在新的类上写我们的内容。 3>它的调用顺序Init(), Run(), Exit()。 4>如果初始化失败的话,线程将会停止执行并返回一个错误代码。 5>如果初始化成功,Run()函数内将是我们写逻辑的位置。 6>退出线程的话通过调用Exit()来进行清理。

其实这个线程如果我们不做管理的话,在Run()执行了return 0之后接着就会到线程Exit()的部分,线程退出并且被清理。
不封装的代码,对FRunnable的使用,只是使用,不好看哈(cpp里面我还加了一个线程和线程的切换,其实那是下面要介绍的),但是相信也能做一个参考:
头文件
// Fill out your copyright notice in the Description page of Project Settings. //tianhuajian/whitetian made in 2021-06-30 10:41:24 //UE4 线程1: 创建一个新的独立线程#pragma once#include "CoreMinimal.h" #include "HAL/Runnable.h"DECLARE_DELEGATE(FMySingleRunnableDelegate); class FMySingleRunnable : public FRunnable { public: FMySingleRunnable(); //线程的初始化, 放你的初始化代码 如果return false那么不会执行Run virtual bool Init(); //执行你的业务逻辑 virtual uint32 Run(); //线程暂停 virtual void Stop(); //线程退出 virtual void Exit(); //创建我的线程 void MyCreate(); //我的独立线程对象 FRunnableThread* pThread; FMySingleRunnableDelegate m_del; };

cpp文件
// Fill out your copyright notice in the Description page of Project Settings.#include "MyRunnableTest.h" #include "GameWorldBaseGameModeBase.h"FMySingleRunnable::FMySingleRunnable() : pThread(nullptr) {}bool FMySingleRunnable::Init() { UUtilsLibrary::Log(TEXT("tianhuajian_standlone_thread Init"), true); return true; }uint32 FMySingleRunnable::Run() { //写你在这个线程的业务逻辑, 测试代码 UUtilsLibrary::Log(TEXT("tianhuajian_standlone_thread Run 1"), true); //线程和线程之间切换 FGraphEventRef Task = FFunctionGraphTask::CreateAndDispatchWhenReady([&]() { UUtilsLibrary::Log(TEXT("change thread"), true); }, TStatId(), nullptr, ENamedThreads::GameThread); FTaskGraphInterface::Get().WaitUntilTaskCompletes(Task); UUtilsLibrary::Log(TEXT("tianhuajian_standlone_thread Run 2"), true); return 0; }void FMySingleRunnable::Stop() { UUtilsLibrary::Log(TEXT("tianhuajian_standlone_thread Stop"), true); }void FMySingleRunnable::Exit() { //UUtilsLibrary::Log(TEXT("tianhuajian_standlone_thread Exit"), true); }void FMySingleRunnable::MyCreate() { pThread = FRunnableThread::Create(this, TEXT("tianhuajian_standlone_thread"), 0, TPri_Normal); }

测试用例
class FMySingleRunnable* pThrad; //测试独立线程 pThrad = new FMySingleRunnable(); pThrad->m_del.BindUObject(this, &AGameWorldBaseGameModeBase::PrintF); pThrad->MyCreate();

在DoWork的地方断点一下,看看是不是你的线程。
下面介绍一下图标线程的基础使用方式
2>TGraphTask
这个就是一套异步的处理方案, 一是用的时候允许你指定一个你想执行你的Gameplay的线程, 二是允许你指定任务的执行顺序。 关于任务执行顺序特点:它支持任务的顺序,它可以先执行一个TGraphTask,保存上 对其的引用比如A吧,此时你想执行完前面的A任务才去执行下一个任务TGraphTask任务B。那么这个系统就能帮上忙。 我搜了一下引擎内部TGraphTask用的地方非常多,我们熟悉的Tick其实最终就是通我们这个图标任务执行的 我随便找个Actor或组件Tick段个点,请看截图

UE4|UE4/UE5 多线程开发 附件插件下载地址
文章图片

当然你可以不指定让它有顺序,只要在模板方法那写一下枚举即可
需要依赖你就定义成FireAndForget,不需要依赖就定义成TrackSubsequents。
static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; }namespace ESubsequentsMode { enum Type { /** 当另一个任务将依赖于此任务时是必要的. */ TrackSubsequents, /** 可以用来节省任务图开销时,发射的任务将不是一个其他任务的依赖. */ FireAndForget }; }

上面说的可能还不知道是个啥
下面我列一下这个的基础用法,我的测试代码,同样也是没经过封装的,可以参考
头文件
// Fill out your copyright notice in the Description page of Project Settings. //tianhuajian/whitetian made in 2021-06-30 10:41:24 //UE4 线程2: FGraphTask, 使用闲置线程#pragma once#include "CoreMinimal.h" #include "GameWorldBaseGameModeBase.h"class FMyGraphTaskTest { public: FMyGraphTaskTest(); ENamedThreads::Type TargetThread; TFunction TheTask; FMyGraphTaskTest(ENamedThreads::Type Thread, TFunction&& Task) : TargetThread(Thread), TheTask(MoveTemp(Task)) { } void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { TheTask(); //写逻辑的地方 UUtilsLibrary::Log(TEXT("SADSADSA"), true); } static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; } //谁闲置用谁 ENamedThreads::Type GetDesiredThread() { return (TargetThread)/*ENamedThreads::AnyThread*/; } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FMyGraphTaskTest, STATGROUP_TaskGraphTasks); } };

CPP文件
构造里面啥都没,其实是可以传入一些参数给到这里面的
// Fill out your copyright notice in the Description page of Project Settings.#include "MyGraphTaskTest.h"FMyGraphTaskTest::FMyGraphTaskTest() {}

测试用例
//指定任意线程去执行我们的逻辑 int32 a = 112134; TGraphTask::CreateTask(NULL, ENamedThreads::AnyThread).ConstructAndDispatchWhenReady(ENamedThreads::AnyThread, [a]() { UUtilsLibrary::Log(FString::FromInt(a), true); });

他有什么缺点吗?
个人的一个见解,也是我测试发现的,比如我指定一个线程(比如AnyThread)去执行我的GamePlay,但是这个时候有可能他会给我指上我的
MainThread,也是就是主线程/游戏线程。我要是执行是个复杂逻辑,那么这时候刚好分配到主线程执行了,那么此时就会造成主线程阻塞。
不知道为什么没有设计一个叫IdelThread,空闲的线程枚举(像AnyThread/GameThread等枚举的定义);
所以还是建议别在这里面指定AnyThread的时候去执行太复杂的逻辑,因为有可能会阻塞你的游戏线程。
3>FAsyncTask
这个系统其实就是UE4提供的线程池了。继承自FNonAbandonableTask。可以支持我们并行的执行复杂计算。 他的实现原理在引擎Init的时候根据CPU核数等(具体我也没太细纠它根据到底哪些标准)创建出一个线程池。 这个线程池里面还有一个队列的概念Queue。分析1:当线程池内的线程在执行完的时候,就将其挂起,标志成空闲状态; 分析2:其他的就是正在运行的状态; 情况1:这个时候再有逻辑的进来的时候,会判断线程池内是否有空闲线程,如果有,将逻辑给到这个空闲线程上, 并将其唤醒,执行我们的逻辑; 情况2:这个时候再有逻辑的进来的时候,线程都忙着呢,将你的逻辑添加到queue里头,一直判断,有没有空闲线程? 有吗?有的话将我队列的最后一个给我拿出来,放到空闲线程上执行,并将这个逻辑在队列里头删掉。

我的线程池也是根据思路做的。
那么怎么用
头文件
#pragma once#include "CoreMinimal.h"class TaskAsyncTask : public FNonAbandonableTask { friend class FAsyncTask; int32 InstanceInt; TaskAsyncTask( int32 _InstanceInt) :InstanceInt(_InstanceInt) { } void DoWork() { UE_LOG(LogTemp, Log, TEXT("DoWork %d"), InstanceInt); } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(TaskAsyncTask, STATGROUP_ThreadPoolAsyncTasks); } };

CPP文件,不用怀疑,cpp里头就是没东西的,也可以不写它
// Fill out your copyright notice in the Description page of Project Settings.#include "MyAsyncTaskTest.h"

测试用例
StartBackgroundTask()异步操作
StartSynchronousTask()同步操作
FAsyncTask *MyTask = new FAsyncTask(3); // MyTask->StartBackgroundTask(); //异步 当前线程不会阻塞 MyTask->StartSynchronousTask(); //同步, 当前线程会阻塞if (MyTask->IsDone()) { UE_LOG(LogTemp, Log, TEXT("MyTask->IsDone()")); }MyTask->EnsureCompletion(); delete MyTask;

测试用例2, UE提供的快捷调用方式
AsyncTask(ENamedThreads::AnyThread, [&]() { UUtilsLibrary::Log("test ue function"); });

测试用例3, UE提供的快捷调用方式
这种方式是不是简单。要是封装个宏绑定个代理就更简单了。所以我在插件里面做了这个工作。
//异步执行 (new FAutoDeleteAsyncTask(Delegate))->StartBackgroundTask(); //同步执行 (new FAutoDeleteAsyncTask(Delegate))->StartSynchronousTask();

4.我的插件 下载地址
用我写的线程池,执行一个Raw、Lambda、WeakLambda、Static、UFunction、UObject、SP、ThreadSafeSP
Raw:C++原生方法
SP:UE4智能指针fast模式的类
ThreadSafeSP:UE4智能指针threadsafe模式的类
这个是lambda的例子
void ARuntimeActor::AsyncInit(FModelMesh* mesh) { auto lambda = [&, mesh]() { Init(mesh); }; GEKThread::GetPoolTask().CreateAsyncLambda(lambda); }

4.1新建线程池的->同异步调用方式
//新建线程池的->异步方法 GEKThread::GetPoolTask().CreateAsyncRaw(***); GEKThread::GetPoolTask().CreateAsyncLambda(***); GEKThread::GetPoolTask().CreateAsyncWeakLambda(***); GEKThread::GetPoolTask().CreateAsyncStatic(***); GEKThread::GetPoolTask().CreateAsyncUFunction(***); GEKThread::GetPoolTask().CreateAsyncUObject(***); GEKThread::GetPoolTask().CreateAsyncSP(***); GEKThread::GetPoolTask().CreateAsyncThreadSafeSP(***); //新建线程池的->异步方法 GEKThread::GetPoolTask().CreateSyncRaw(***); GEKThread::GetPoolTask().CreateSyncLambda(***); GEKThread::GetPoolTask().CreateSyncWeakLambda(***); GEKThread::GetPoolTask().CreateSyncStatic(***); GEKThread::GetPoolTask().CreateSyncUFunction(***); GEKThread::GetPoolTask().CreateSyncUObject(***); GEKThread::GetPoolTask().CreateSyncSP(***); GEKThread::GetPoolTask().CreateSyncThreadSafeSP(***);

4.2 封装UE4提供的线程池的->同异步调用方式
//封装UE4提供的线程池的->异步方法 GEKThread::GetAsyncTask().CreateAsyncRaw(***); GEKThread::GetAsyncTask().CreateAsyncLambda(***); GEKThread::GetAsyncTask().CreateAsyncWeakLambda(***); GEKThread::GetAsyncTask().CreateAsyncStatic(***); GEKThread::GetAsyncTask().CreateAsyncUFunction(***); GEKThread::GetAsyncTask().CreateAsyncUObject(***); GEKThread::GetAsyncTask().CreateAsyncSP(***); GEKThread::GetAsyncTask().CreateAsyncThreadSafeSP(***); //封装UE4提供的线程池的->异步方法 GEKThread::GetAsyncTask().CreateSyncRaw(***); GEKThread::GetAsyncTask().CreateSyncLambda(***); GEKThread::GetAsyncTask().CreateSyncWeakLambda(***); GEKThread::GetAsyncTask().CreateSyncStatic(***); GEKThread::GetAsyncTask().CreateSyncUFunction(***); GEKThread::GetAsyncTask().CreateSyncUObject(***); GEKThread::GetAsyncTask().CreateSyncSP(***); GEKThread::GetAsyncTask().CreateSyncThreadSafeSP(***);

4.3 EventGraph使用宏 可以执行你的逻辑在你指定的线程上,并且可以传递一个前置你要等待的GraphEvent线程逻辑,指定顺序。
//FGraphTask使用定义: 具体定义可参考EKThreadGraphManager.h的头部注释 #pragma region Macro_FEKGraphTask_MacroDefine//切换到指定线程(CallThreadName)上执行我们的代理 (PS:不要调用该方法,该方法主要给下面的绑定提供的.直接调用下面的即可) 支持任务等待的 #define EK_CALL_THREAD(WaitOtherGraphTask, CallThreadName, InTaskDeletegate) \ FSimpleDelegateGraphTask::CreateAndDispatchWhenReady(InTaskDeletegate, TStatId(), WaitOtherGraphTask, CallThreadName); //3.1 切换到指定线程(CallThreadName)上执行我们的Raw代理 #define EK_CALL_THREAD_RAW(WaitOtherGraphTask, CallThreadName, Object, ...) \ EK_CALL_THREAD(WaitOtherGraphTask, CallThreadName, FSimpleDelegate::CreateRaw(Object, __VA_ARGS__))//3.2 切换到指定线程(CallThreadName)上执行我们的Lambda #define EK_CALL_THREAD_LAMBDA(WaitOtherGraphTask, CallThreadName, ...) \ EK_CALL_THREAD(WaitOtherGraphTask, CallThreadName, FSimpleDelegate::CreateLambda(__VA_ARGS__))//3.3 切换到指定线程(CallThreadName)上执行我们的WeakLambda #define EK_CALL_THREAD_WEAKLAMBDA(WaitOtherGraphTask, CallThreadName, ...) \ EK_CALL_THREAD(WaitOtherGraphTask, CallThreadName, FSimpleDelegate::CreateWeakLambda(__VA_ARGS__))//3.4 切换到指定线程(CallThreadName)上执行我们的Static #define EK_CALL_THREAD_STATIC(WaitOtherGraphTask, CallThreadName, ...) \ EK_CALL_THREAD(WaitOtherGraphTask, CallThreadName, FSimpleDelegate::CreateStatic(__VA_ARGS__))//3.5 切换到指定线程(CallThreadName)上执行我们的UFunction代理 #define EK_CALL_THREAD_UFUNCTION(WaitOtherGraphTask, CallThreadName, Object, ...) \ EK_CALL_THREAD(WaitOtherGraphTask, CallThreadName, FSimpleDelegate::CreateUFunction(Object, __VA_ARGS__))//3.6 切换到指定线程(CallThreadName)上执行我们的UObject代理 #define EK_CALL_THREAD_UOBJECT(WaitOtherGraphTask, CallThreadName, Object, ...) \ EK_CALL_THREAD(WaitOtherGraphTask, CallThreadName, FSimpleDelegate::CreateUObject(Object, __VA_ARGS__))//3.7 切换到指定线程(CallThreadName)上执行我们的SPFast代理 #define EK_CALL_THREAD_SP(WaitOtherGraphTask, CallThreadName, Object, ...) \ EK_CALL_THREAD(WaitOtherGraphTask, CallThreadName, FSimpleDelegate::CreateSP(Object, __VA_ARGS__))//3.8 切换到指定线程(CallThreadName)上执行我们的SPSafe代理 #define EK_CALL_THREAD_SPSAFE(WaitOtherGraphTask, CallThreadName, Object, ...) \ EK_CALL_THREAD(WaitOtherGraphTask, CallThreadName, FSimpleDelegate::CreateThreadSafeSP(Object, __VA_ARGS__))//等一个任务,UE4这个命名反的.注意这个会阻塞. #define EK_WAITING_OTHER_THREAD_SINGLE_COMPLETED(EventRef) FTaskGraphInterface::Get().WaitUntilTaskCompletes(EventRef) //等一个数组任务,UE4这个命名反的.注意这个会阻塞. #define EK_WAITING_OTHER_THREADS_ARRAY_COMPLETED(EventRef) FTaskGraphInterface::Get().WaitUntilTasksComplete(EventRef)#pragma endregion

4.4 资源同异步加载 还没测试好,不太好用,还在改进,插件持续改进过程中
GEKThread::GetStreamble().CreateAsyncRaw(***); GEKThread::GetStreamble().CreateAsyncLambda(***); GEKThread::GetStreamble().CreateAsyncWeakLambda(***); GEKThread::GetStreamble().CreateAsyncStatic(***); GEKThread::GetStreamble().CreateAsyncUFunction(***); GEKThread::GetStreamble().CreateAsyncUObject(***); GEKThread::GetStreamble().CreateAsyncSP(***); GEKThread::GetStreamble().CreateAsyncThreadSafeSP(***);

UE4|UE4/UE5 多线程开发 附件插件下载地址
文章图片

5.几点注意项 比如下面的代码,我是想在主线程执行完这个创建UI的逻辑,并且要等着他执行完。你能看出下面逻辑有哪些隐患吗?
下面这个逻辑在非GameThread的线程执行一点问题没有,但但是如果这个方法执行在了主线程里面,又去等待主线程执行完,这个时候就把主线程卡死了。
//检查进度条UI的合法性 void USPGameInstance::CheckProgressUI() { if (!IsValid(m_pProgress)) { //如果是UI需要创建的话, 到主线程去创建它. 并且创建完才能继续往下. auto CreateUIFunc = EK_CALL_THREAD_LAMBDA(nullptr, ENamedThreads::GameThread, [&]() { //创建全局的进度条 auto widget = GGameInstance->UIManager()->OpenUI(UI_DoubleMode_Progress); m_pProgress = Cast(widget); }); EK_WAITING_OTHER_THREAD_SINGLE_COMPLETED(CreateUIFunc); } }

所以我们需要注意:
1>不再你的执行逻辑的线程,再去等待当前线程;比如已经在主线程,又去等待主线程;错!
2>如下的介绍吧,网上很多了
网上应该很多都介绍了,不要在非GameThread做下面这些事。其实这也是我另一篇博客中总结的,暂时转成私密了。
UE4|UE4/UE5 多线程开发 附件插件下载地址
文章图片

6.需要做的思考 线程的创建是占用栈空间的,再甜的糖都不能一直吃,不能无限制的创建。
【UE4|UE4/UE5 多线程开发 附件插件下载地址】谢谢,创作不易,大侠请留步… 动起可爱的双手,来个赞再走呗 <( ̄︶ ̄)>
?( ′???` )比心

    推荐阅读