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

32 篇文章 43 订阅

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

前言

这几天在做一些资源的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<void()> TheTask;

	FMyGraphTaskTest(ENamedThreads::Type Thread, TFunction<void()>&& 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<FMyGraphTaskTest>::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<TaskAsyncTask>;

	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<TaskAsyncTask> *MyTask = new FAsyncTask<TaskAsyncTask>(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<TaskAsyncTask>(Delegate))->StartBackgroundTask();

//同步执行
(new FAutoDeleteAsyncTask<TaskAsyncTask>(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<USPProgress>(widget);
		});
		EK_WAITING_OTHER_THREAD_SINGLE_COMPLETED(CreateUIFunc);
	}
}

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

6.需要做的思考

线程的创建是占用栈空间的,再甜的糖都不能一直吃,不能无限制的创建。

谢谢,创作不易,大侠请留步… 动起可爱的双手,来个赞再走呗 <( ̄︶ ̄)>
ღ( ´・ᴗ・` )比心

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WhiteTian

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值