UE4 简单异步线程使用笔记
遥远_y
2022年04月11日 15:40
收录于文集
共4篇

总会遇到需要进行大量计算的需求,为了不影响主线程,导致画面卡顿,这种时候就可以考虑使用异步线程进行计算。

这里记录一下简单异步线程的使用。

用一个简单例子来说明:

1、在蓝图里某个地方调用一个进行异步操作的函数

2、异步操作的结果在同一个蓝图的某个事件返回

如下图,在 BeginPlay 后调用异步操作的函数, Event Print Num 就是异步操作返回的结果。

图1

这个异步操作里做的事情很简单,就是返回一个字符串,这个字符串的内容是从0到100000的数字拼接。举例:从0到5的字符串就是:"012345&#​34;。100000只是为了模拟数据很大。

以上交代了要做的事以及最后在蓝图里的调用样子。下面来说代码怎么写。

创建一个继承自 Actor 的 C++ 类 MyActor,上面的蓝图就是继承自 MyActor 的。

先贴出 MyActor 的代码,后面再解释代码。

MyActor.h

代码块
C++
自动换行
复制代码
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Async/TaskGraphInterfaces.h"
#include "MyActor.generated.h"



class FJustPrintTask
{
	FString TaskName;
	AActor* TaskOwner;

public:
	FJustPrintTask() {}
	FJustPrintTask(FString name, AActor* owner) :TaskName(name), TaskOwner(owner) {}
	~FJustPrintTask() {}

	FORCEINLINE TStatId GetStatId() const
	{
		RETURN_QUICK_DECLARE_CYCLE_STAT(FJustPrintTask, STATGROUP_TaskGraphTasks);
	}

	static ENamedThreads::Type GetDesiredThread()
	{
		return ENamedThreads::GameThread;
	}

	static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; }
	void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent);
};



UCLASS()
class TASKGRAPHTEST_API AMyActor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AMyActor();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	UFUNCTION(BlueprintCallable)
		void AsyncPrint();

	UFUNCTION(BlueprintImplementableEvent)
		void PrintNum(const FString& num);
};
复制成功

MyActor.cpp

代码块
C++
自动换行
复制代码
#include "MyActor.h"

// Sets default values
AMyActor::AMyActor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
}

// Called when the game starts or when spawned
void AMyActor::BeginPlay()
{
	Super::BeginPlay();
}

// Called every frame
void AMyActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

void AMyActor::AsyncPrint()
{
	TGraphTask<FJustPrintTask>::CreateTask(nullptr, ENamedThreads::GameThread).ConstructAndDispatchWhenReady("123", this);
}

void FJustPrintTask::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
	AMyActor* Actor = Cast<AMyActor>(TaskOwner);
	if (IsValid(Actor))
	{
		FString s = "123";
		for (int i = 0; i < 100000; i++)
		{
			s.Append(FString::FromInt(i));
		}
		const FString& num = s;
		Actor->PrintNum(s);
	}
	UE_LOG(LogTemp, Log, TEXT("======= task name = %s"), *TaskName);
}
复制成功

可以看到, MyActor 里只有两个自定义的函数:

代码块
C++
自动换行
复制代码
	UFUNCTION(BlueprintCallable)
		void AsyncPrint();

	UFUNCTION(BlueprintImplementableEvent)
		void PrintNum(const FString& num);
复制成功

PrintNum 没啥好说的,它就是上面蓝图里,用来接收异步操作返回的字符串的。

AsycnPrint 也很简单,就是创建一个异步操作的任务。

代码块
C++
自动换行
复制代码
void AMyActor::AsyncPrint()
{
	TGraphTask<FJustPrintTask>::CreateTask(nullptr, ENamedThreads::GameThread).ConstructAndDispatchWhenReady("123", this);
}
复制成功

里面其实就一句代码,都是套路,其中 FJustPrintTask 是我们自己创建的任务类,其实重点都在这个类里,下面会讲。(这篇文章只是非常简单的对异步操作用法做个记录,不是讲解原理的文章,如果代码里的函数没见过的,在学会使用后应当再去查询讲解异步原理的文章。)

最后面的两个参数 “123” 和 this,都是 FJustPrintTask 类的两个属性,理论上它们应该是要进行处理的数据。前面的 “123” 其实是表示任务的名字,这里只是用来打印出来。this 其实就是 MyActor,即创建任务的对象,它的作用是最后来调用 PrintNum 函数。

现在我们来看 FJustPrintTask 类,也就是我们的任务类。

代码块
C++
自动换行
复制代码
class FJustPrintTask
{
	FString TaskName;
	AActor* TaskOwner;

public:
	FJustPrintTask() {}
	FJustPrintTask(FString name, AActor* owner) :TaskName(name), TaskOwner(owner) {}
	~FJustPrintTask() {}

	FORCEINLINE TStatId GetStatId() const
	{
		RETURN_QUICK_DECLARE_CYCLE_STAT(FJustPrintTask, STATGROUP_TaskGraphTasks);
	}

	static ENamedThreads::Type GetDesiredThread()
	{
		return ENamedThreads::GameThread;
	}

	static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; }
	void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent);
};
复制成功

注意到最后一个函数 DoTask ,它是具体进行计算任务的函数。

这个类的属性自定义,意思就是,你需要什么数据,就定义什么属性,也就是上面说的,这个类的属性应该是要进行计算的数据。这里只有 TaskName 和 TaskOwner,作用上面说了。

构造函数和析构函数就没啥好说的了。

而其他函数,除了 DoTask 的定义,其他所有函数的声明和定义都是套路,照抄就行。

注意,函数名等要严格一致,否则编译不过。

最后我们看 DoTask 这个函数的定义:

代码块
C++
自动换行
复制代码
void FJustPrintTask::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
	AMyActor* Actor = Cast<AMyActor>(TaskOwner);
	if (IsValid(Actor))
	{
		FString s = "123";
		for (int i = 0; i < 100000; i++)
		{
			s.Append(FString::FromInt(i));
		}
		const FString& num = s;
		Actor->PrintNum(s);
	}
	UE_LOG(LogTemp, Log, TEXT("======= task name = %s"), *TaskName);
}
复制成功

灰常简单,就是拼字符串,和打印一个 log。

稍微需要注意的是,TaskOwner 这里用到了,作用就是强制转换成创建任务的类 MyActor,然后调用 PrintNum 函数,把拼好的字符串返回。

到这里异步拼接字符串的事情就做完了。

来看下最后输出吧:

那个字符串死长的。

最后,如果你有兴趣,可以试着在蓝图里直接拼接字符串,看看画面是不是卡住了,再用异步操作做个对比,就会感觉到它的好用了。