原创文章,转载请注明出处。
虚幻引擎 多线程开发介绍
- 前言
- 封装的插件下载地址:
- 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段个点,请看截图
文章图片
当然你可以不指定让它有顺序,只要在模板方法那写一下枚举即可
需要依赖你就定义成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(***);
文章图片
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做下面这些事。其实这也是我另一篇博客中总结的,暂时转成私密了。
文章图片
6.需要做的思考 线程的创建是占用栈空间的,再甜的糖都不能一直吃,不能无限制的创建。
【UE4|UE4/UE5 多线程开发 附件插件下载地址】谢谢,创作不易,大侠请留步… 动起可爱的双手,来个赞再走呗 <( ̄︶ ̄)>
?( ′???` )比心
推荐阅读
- C++|UE4智能指针TSharedPtr的几种初始化方式
- 使用虚幻引擎中的C++导论
- Scripting the Editor using Python
- 虚幻4DPI自适应缩放规则解析
- 【UE4_C++】<14-3>用户界面 UI和UMG——为UI创建屏幕尺寸自适应缩放
- UE4血条、名字面向相机
- UE4|UE4C++ 设置UMG控件的Slot