总会遇到需要进行大量计算的需求,为了不影响主线程,导致画面卡顿,这种时候就可以考虑使用异步线程进行计算。
这里记录一下简单异步线程的使用。
用一个简单例子来说明:
1、在蓝图里某个地方调用一个进行异步操作的函数
2、异步操作的结果在同一个蓝图的某个事件返回
如下图,在 BeginPlay 后调用异步操作的函数, Event Print Num 就是异步操作返回的结果。

图1
这个异步操作里做的事情很简单,就是返回一个字符串,这个字符串的内容是从0到100000的数字拼接。举例:从0到5的字符串就是:"012345"。100000只是为了模拟数据很大。
以上交代了要做的事以及最后在蓝图里的调用样子。下面来说代码怎么写。
创建一个继承自 Actor 的 C++ 类 MyActor,上面的蓝图就是继承自 MyActor 的。
先贴出 MyActor 的代码,后面再解释代码。
MyActor.h
#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
#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 里只有两个自定义的函数:
UFUNCTION(BlueprintCallable)
void AsyncPrint();
UFUNCTION(BlueprintImplementableEvent)
void PrintNum(const FString& num); PrintNum 没啥好说的,它就是上面蓝图里,用来接收异步操作返回的字符串的。
AsycnPrint 也很简单,就是创建一个异步操作的任务。
void AMyActor::AsyncPrint()
{
TGraphTask<FJustPrintTask>::CreateTask(nullptr, ENamedThreads::GameThread).ConstructAndDispatchWhenReady("123", this);
} 里面其实就一句代码,都是套路,其中 FJustPrintTask 是我们自己创建的任务类,其实重点都在这个类里,下面会讲。(这篇文章只是非常简单的对异步操作用法做个记录,不是讲解原理的文章,如果代码里的函数没见过的,在学会使用后应当再去查询讲解异步原理的文章。)
最后面的两个参数 “123” 和 this,都是 FJustPrintTask 类的两个属性,理论上它们应该是要进行处理的数据。前面的 “123” 其实是表示任务的名字,这里只是用来打印出来。this 其实就是 MyActor,即创建任务的对象,它的作用是最后来调用 PrintNum 函数。
现在我们来看 FJustPrintTask 类,也就是我们的任务类。
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 这个函数的定义:
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 函数,把拼好的字符串返回。
到这里异步拼接字符串的事情就做完了。
来看下最后输出吧:

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