【UE·引擎篇】Runnable、TaskGraph、AsyncTask、Async多线程开发指南_ue5 asynctask-CSDN博客
案例介绍(Runnable)
Runnable来实现多线程是最基础的用法,与后面介绍的其他用法来说,它没有什么复杂的功能。
从自定义Actor子类ATestRunnableActor里获取一个数字,然后在多线程里实现一个计数器,当计数器大于这个数字时,线程退出。
代码实现
首先需要继承FRunnable实现我们的线程执行体:
.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestRunnableActor.generated.h"
UCLASS()
class LEARNMULTITHREADING_API ATestRunnableActor : public AActor
{
GENERATED_BODY()
public:
ATestRunnableActor();
protected:
virtual void BeginPlay() override;
public:
//从0开始的计数器
int32 TestCount;
UPROPERTY(EditAnywhere)
int32 TestTarget;
};
.cpp
#include "TestRunnableActor.h"
#include "TestRunnable.h"
ATestRunnableActor::ATestRunnableActor()
{
PrimaryActorTick.bCanEverTick = true;
}
/*
这里需要注意如果我们同时在多个线程里去读和写Actor的数据会引起线程不同步的问题,需要加锁FScopeLock。
然后创建一个线程类FRunnableThread来使用FTestRunnable:
*/
void ATestRunnableActor::BeginPlay()
{
Super::BeginPlay();
FTestRunnable* Runnable1 = new FTestRunnable(TEXT("线程1"), this);
FTestRunnable* Runnable2 = new FTestRunnable(TEXT("线程2"), this);
FRunnableThread* RunnableThread1 = FRunnableThread::Create(Runnable1, *Runnable1->MyThreadName);
FRunnableThread* RunnableThread2 = FRunnableThread::Create(Runnable2, *Runnable2->MyThreadName);
}
Runnable代码
.h
class FTestRunnable : public FRunnable
{
public:
FTestRunnable(FString ThreadName, class ATestRunnableActor* TestActor): MyThreadName(ThreadName),
Tester(TestActor)
{
}
virtual bool Init() override;
virtual uint32 Run() override;
virtual void Exit() override;
FString MyThreadName;
class ATestRunnableActor* Tester;
private:
int32 WorkCount = 0;
//线程临界区,用于线程加锁
static FCriticalSection CriticalSection;
};
.cpp
#include "TestRunnable.h"
#include "TestRunnableActor.h"
FCriticalSection FTestRunnable::CriticalSection;
bool FTestRunnable::Init()
{
UE_LOG(LogTemp, Log, TEXT("%s初始化"), *MyThreadName);
return IsValid(Tester);
}
uint32 FTestRunnable::Run()
{
while (IsValid(Tester))
{
#if true // thread sync 线程同步
FScopeLock Lock(&CriticalSection);
#endif
if (Tester->TestCount < Tester->TestTarget)
{
Tester->TestCount++;
WorkCount++;
if (WorkCount % 100 == 0)
UE_LOG(LogTemp, Log, TEXT("%s %d"), *MyThreadName, WorkCount);
}
else
{
break;
}
}
return 0;
}
void FTestRunnable::Exit()
{
UE_LOG(LogTemp, Log, TEXT("%s执行了%d次"), *MyThreadName, WorkCount);
}
总结
FRunnable(线程执行体)和FRunnableThread(线程类)是最简单的实现多线程方式,它只有创建、暂停、销毁、等待完成等基础功能。在实战中也较少用到。
案例介绍(TaskGraph)
TaskGraph任务图,是用来解决多线程中任务需要先后执行顺序的问题。
我们以游戏开发中工作流为例:
- 首先是策划提出需求案子
- 然后美术设计概念图
- 模型师根据概念图建模
- 动画师等建模完成后制作动画
- 程序在案子提出后开发特性
- 等上面全部完成后策划进行验收
这是最简单的情况,现在程序特性比较复杂,将分给三个程序员分别开发(即子任务)。
同时这个需求非常重要,老板很关心开发进度,上面的每一步做完之后都要跟他汇报才算真正完成。这个需要使用TaskGraph的DontCompleteUntil功能。
代码实现
FWorkTask和FReportTask
先创建两个任务,一个表示工作内容FWorkTask,一个表示汇报FReportTask。
WorkTask.h
class FWorkTask
{
FString TaskName;
TArray<TGraphTask<FWorkTask>*> ChildTasks;
AActor* TaskOwner;
public:
FWorkTask(FString Name, TArray<TGraphTask<FWorkTask>*> Children, AActor* Actor) : TaskName(Name),
ChildTasks(Children), TaskOwner(Actor)
{}
~FWorkTask()
{}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FWorkTask, STATGROUP_TaskGraphTasks);
}
static ENamedThreads::Type GetDesiredThread()
{
return ENamedThreads::AnyThread;
}
//ESubsequentsMode::TrackSubsequents 追踪完成状态,一般用这个
//ESubsequentsMode::FireAndForget 做了以后无法得知是否完成,只有没有任何依赖的Task才用。亲测用了这个容易崩溃。
static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; }
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent);
};
- GetStatId。固定写法。RETURN_QUICK_DECLARE_CYCLE_STAT第一个参数为类名。
- GetDesiredThread。可以指定使用哪种线程。除了AnyThread还有GameThread、RHIThread等多种线程设置。
- GetSubsequentsMode。任务完成模式。
- ESubsequentsMode::TrackSubsequents 追踪完成状态,一般用这个
- ESubsequentsMode::FireAndForget 做了以后无法得知是否完成,只有没有任何依赖的Task才用。
- DoWork。任务要处理的事情。
WorkTask.cpp
#include "WorkTask.h"
#include "ReportTask.h"
void FWorkTask::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__ " %s:Begin"), *TaskName);
for (TGraphTask<FWorkTask>* Task : ChildTasks)
{
if (Task)
{
Task->Unlock();
MyCompletionGraphEvent->DontCompleteUntil(Task->GetCompletionEvent());
}
}
// do something
MyCompletionGraphEvent->DontCompleteUntil(TGraphTask<FReportTask>::CreateTask().
ConstructAndDispatchWhenReady(TaskName, TaskOwner));
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__ " %s:End"), *TaskName);
}
首先是讲解类型FGraphEventRef是什么。FGraphEventRef是FGraphEvent的指针。FGraphEvent是用来传递任务完成状态的。还是上面那个例子,其实每个岗位的人并不需要知道上游岗位的人具体做了什么工作内容,只需要知道对方完成了没有。如果完成了那么开始我的工作,如果我完成了,我把我完成的事件传递给我的下游。这就是FGraphEvent的主要职责。
回到Dowork函数:
- 调用子任务的Unlock让它开始工作。
- GraphTask<T>::CreateTask().ConstructAndDispatchWhenReady是创建任务,后面会细讲。
- DontCompleteUntil表示只有别的任务完成了,我才算完成。
ReportTask.h
class FReportTask
{
FString TaskName;
AActor* TaskOwner;
public:
FReportTask(FString Name, AActor* Actor) : TaskName(Name), TaskOwner(Actor)
{
}
~FReportTask()
{
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FReportTask, STATGROUP_TaskGraphTasks);
}
static ENamedThreads::Type GetDesiredThread()
{
return ENamedThreads::GameThread;
}
static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; }
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent);
};
ReportTask.cpp
#include "ReportTask.h"
#include "TestTaskGraphActor.h"
void FReportTask::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
ATestTaskGraphActor* Actor = Cast<ATestTaskGraphActor>(TaskOwner);
if (IsValid(Actor))
{
Actor->OnTaskComplete(TaskName);
}
}
TaskGraph.h
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestTaskGraphActor.generated.h"
USTRUCT(BlueprintType)
struct FTaskItem
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(BlueprintReadWrite)
FString TaskName;
FGraphEventRef GraphEvent;
TGraphTask<class FWorkTask>* GraphTask;
FTaskItem()
: TaskName(TEXT("NoName")), GraphEvent(nullptr), GraphTask(nullptr)
{
}
FTaskItem(FString Name, FGraphEventRef EventRef = FGraphEventRef())
: TaskName(Name), GraphEvent(EventRef), GraphTask(nullptr)
{
}
FTaskItem(FString Name, TGraphTask<class FWorkTask>* Task = nullptr)
: TaskName(Name), GraphEvent(nullptr), GraphTask(Task)
{
}
~FTaskItem() { GraphEvent = nullptr; }
};
UCLASS()
class LEARNMULTITHREADING_API ATestTaskGraphActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ATestTaskGraphActor();
public:
UFUNCTION(BlueprintCallable)
FTaskItem CreateTask(FString TaskName, const TArray<FTaskItem>& Prerequisites,
const TArray<FTaskItem>& ChildTasks, bool DispatchWhenReady = true);
UFUNCTION(BlueprintCallable)
FTaskItem CreateTaskPure(FString TaskName, bool DispatchWhenReady = true)
{
const TArray<FTaskItem> Empty;
return CreateTask(TaskName, Empty, Empty, DispatchWhenReady);
}
UFUNCTION(BlueprintCallable)
FTaskItem CreateTaskWithPrerequisitesOnly(FString TaskName, const TArray<FTaskItem>& Prerequisites,
bool DispatchWhenReady = true)
{
return CreateTask(TaskName, Prerequisites, TArray<FTaskItem>(), DispatchWhenReady);
}
UFUNCTION(BlueprintCallable)
FTaskItem CreateTaskWithChildTasksOnly(FString TaskName, const TArray<FTaskItem>& ChildTasks,
bool DispatchWhenReady = true)
{
return CreateTask(TaskName, TArray<FTaskItem>(), ChildTasks, DispatchWhenReady);
}
UFUNCTION(BlueprintCallable)
void FireTask(const FTaskItem& Task);
void OnTaskComplete(const FString& TaskName);
};
CreatTask(创建任务)、FireTask(运行任务)、OnTaskComplete(任务完成回调)。
TaskGraph.cpp
#include "TestTaskGraphActor.h"
#include "WorkTask.h"
// Sets default values
ATestTaskGraphActor::ATestTaskGraphActor()
{
// 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;
}
FTaskItem ATestTaskGraphActor::CreateTask(FString TaskName, const TArray<FTaskItem>& Prerequisites,
const TArray<FTaskItem>& ChildTasks, bool DispatchWhenReady)
{
FGraphEventArray PrerequisiteEvents;
TArray<TGraphTask<FWorkTask>*> ChildEvents;
UE_LOG(LogTemp, Log, TEXT("Task[%s] is Created."), *TaskName);
if (Prerequisites.Num() > 0)
{
PrerequisiteEvents.Reserve(Prerequisites.Num());
for (FTaskItem Item : Prerequisites)
{
if (Item.GraphTask)
{
PrerequisiteEvents.Add(Item.GraphTask->GetCompletionEvent());
UE_LOG(LogTemp, Log, TEXT("Task[%s] wait Task[%s]"), *TaskName, *Item.TaskName);
}
else if (Item.GraphEvent.IsValid())
{
PrerequisiteEvents.Add(Item.GraphEvent);
UE_LOG(LogTemp, Log, TEXT("Task[%s] wait Task[%s]"), *TaskName, *Item.TaskName);
}
}
}
if (ChildTasks.Num() > 0)
{
ChildEvents.Reserve(ChildTasks.Num());
for (FTaskItem Item : ChildTasks)
{
if (Item.GraphTask)
{
ChildEvents.Add(Item.GraphTask);
UE_LOG(LogTemp, Log, TEXT("Task[%s] will execute after Task[%s]"), *Item.TaskName, *TaskName);
}
}
}
if (DispatchWhenReady)
{
//ConstructAndDispatchWhenReady返回的是Event
return FTaskItem(TaskName, TGraphTask<FWorkTask>::CreateTask(&PrerequisiteEvents).
ConstructAndDispatchWhenReady(TaskName, ChildEvents, this));
}
//ConstructAndHold返回的是Task
return FTaskItem(TaskName, TGraphTask<FWorkTask>::CreateTask(&PrerequisiteEvents).
ConstructAndHold(TaskName, ChildEvents, this));
}
void ATestTaskGraphActor::FireTask(const FTaskItem& Task)
{
if(Task.GraphTask)
{
UE_LOG(LogTemp, Log, TEXT("Task[%s] Fire."), *Task.TaskName);
Task.GraphTask->Unlock();
}
}
void ATestTaskGraphActor::OnTaskComplete(const FString& TaskName)
{
UE_LOG(LogTemp, Log, TEXT("Task[%s] is Reported."), *TaskName);
}
- 填充依赖任务事件数组FGraphEventArray
- 填充子任务数组TArray<TGraphTask<FWorkTask>*>
- CreatTask传入FGraphEventArray
- ConstructAndDispatchWhenReady表示任务创建后如果满足条件则立刻执行。可以传任意参数,作为TGraphTask<T>的T的参数。
- ConstructAndHold表示任务创建后不立刻执行,调用Unlock才执行。可以传任意参数,作为TGraphTask<T>的T的参数。
总结
TaskGraph适合有依赖关系的多线程任务。指定使用哪个线程的时候要注意一些逻辑只能在GameThread上调用。如
- 创建、消耗Actor
- Debug绘制函数
- 定时器 TimerManager
案例介绍(AsyncTask)
AsyncTask也可以实现多线程,它可以利用ue4底层的线程池机制来调度任务。
从这里开始包括后面的内容,我们将计算一个1到1000w的开根号,并求和,最后除以1000w的简单逻辑。并且计算主线程执行时长和逻辑计算总时长,来比较不同方法之间的差距。
- 主线程执行时长的计算方式是创建多线程之前记录时间点,然后创建完之后记录时间点,两者相减。
- 逻辑计算总时长是多线程里计算出结果以后的时间点减去创建多线程之前的时间点。
代码实现
AsyncTask.h
/*
*当线程池被销毁的时候,会调用Abandon函数
*继承FNonAbandonableTask的话这个时候就不会丢弃而且等待执行完
*/
class FTestAsyncTask : public FNonAbandonableTask
{
friend class FAutoDeleteAsyncTask<FTestAsyncTask>;
int32 TargetCounter;
double OldTime = 0;
FTestAsyncTask(int32 Target): TargetCounter(Target)
{
}
void DoWork();
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(MyAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
}
};
为什么要继承FNonAbandonableTask?
当线程池被销毁的时候,会调用Abandon函数。继承FNonAbandonableTask的话这个时候就不会丢弃而且等待执行完。如果需要丢弃则不继承,并且自己实现CanAbandon和Abandon函数。源码里可丢弃的任务参考:FAsyncStatsFile。
AsyncTask.cpp
#include "TestAsyncTask.h"
void FTestAsyncTask::DoWork()
{
OldTime = FPlatformTime::Seconds();
double Result = 0;
for (int32 i = 0; i < TargetCounter; i++)
{
float j = i;
Result += FMath::Sqrt(j)/TargetCounter;
}
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait TargetCounterR(%f)"), __LINE__, Result);
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
}
执行Actor.h
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestAsyncActor.generated.h"
UCLASS()
class LEARNMULTITHREADING_API ATestAsyncActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ATestAsyncActor();
protected:
double OldTime;
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int TargetCounter;
UFUNCTION(BlueprintCallable)
void TestAsyncTaskClass();
UFUNCTION(BlueprintCallable)
void TestAsyncTaskClass_Synchronous();
UFUNCTION(BlueprintCallable)
void TestAsyncTaskFunc_AnyThread();
UFUNCTION(BlueprintCallable)
void TestAsyncTaskFunc_GameThread();
UFUNCTION(BlueprintCallable)
void TestAsyncFunc_NoReturn();
UFUNCTION(BlueprintCallable)
void TestAsyncFunc_WithReturn();
UFUNCTION(BlueprintCallable)
void TestAsyncFunc_ParallelFor();
};
执行Actor.cpp
#include "TestAsyncActor.h"
#include "TestAsyncTask.h"
// Sets default values
ATestAsyncActor::ATestAsyncActor()
{
// 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;
}
//FAutoDeleteAsyncTask顾名思义就是任务执行完就会自动删除。
void ATestAsyncActor::TestAsyncTaskClass()
{
OldTime = FPlatformTime::Seconds();
//使用其他线程
(new FAutoDeleteAsyncTask<FTestAsyncTask>(TargetCounter))->StartBackgroundTask();
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}
void ATestAsyncActor::TestAsyncTaskClass_Synchronous()
{
OldTime = FPlatformTime::Seconds();
//使用当前线程
(new FAutoDeleteAsyncTask<FTestAsyncTask>(TargetCounter))->StartSynchronousTask();
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}
void ATestAsyncActor::TestAsyncTaskFunc_AnyThread()
{
OldTime = FPlatformTime::Seconds();
AsyncTask(ENamedThreads::AnyThread, [=]()
{
double Result = 0;
for (int32 i = 0; i < TargetCounter; i++)
{
float j = i;
Result += FMath::Sqrt(j) / TargetCounter;
}
UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, Result);
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT("@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
});
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}
void ATestAsyncActor::TestAsyncTaskFunc_GameThread()
{
OldTime = FPlatformTime::Seconds();
AsyncTask(ENamedThreads::GameThread, [=]()
{
double Result = 0;
for (int32 i = 0; i < TargetCounter; i++)
{
float j = i;
Result += FMath::Sqrt(j) / TargetCounter;
}
UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, Result);
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT("@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
});
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}
void ATestAsyncActor::TestAsyncFunc_NoReturn()
{
//TaskGraphMainThread 计算时间最慢
//Thread 主线程结束时间最慢
OldTime = FPlatformTime::Seconds();
Async(EAsyncExecution::TaskGraph, [=]()
{
double Result = 0;
for (int32 i = 0; i < TargetCounter; i++)
{
float j = i;
Result += FMath::Sqrt(j) / TargetCounter;
}
UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, Result);
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT("@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
});
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}
void ATestAsyncActor::TestAsyncFunc_WithReturn()
{
OldTime = FPlatformTime::Seconds();
TFuture<double> FutureResult = Async(EAsyncExecution::TaskGraph, [=]()
{
double Result = 0;
for (int32 i = 0; i < TargetCounter; i++)
{
float j = i;
Result += FMath::Sqrt(j) / TargetCounter;
}
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT("@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
return Result;
});
//使用Get获取返回值,会阻塞主线程
UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, FutureResult.Get());
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}
//本案例使用ParallerlFor总计算时间会变慢
void ATestAsyncActor::TestAsyncFunc_ParallelFor()
{
OldTime = FPlatformTime::Seconds();
int Internal = TargetCounter / 10;
auto FutureResult = Async(EAsyncExecution::TaskGraph, [=]()
{
TArray<double> ResultArray;
ResultArray.Init(0, 10);
ParallelFor(ResultArray.Num(), [&ResultArray,Internal,this](int32 Index)
{
for (int32 i = Index * Internal; i < (Index + 1) * Internal; i++)
{
float j = i;
ResultArray[Index] += FMath::Sqrt(j)/TargetCounter;
}
});
double Sum = 0;
for (int32 j = 0; j < ResultArray.Num(); j++)
{
Sum += ResultArray[j];
}
UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, Sum);
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT("@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
return Sum;
});
//UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, FutureResult.Get());
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}
还有StartBackgroundTask和StartSynchronousTask的区别:
- StartBackgroundTask会利用线程池里空闲的线程来执行。
- StartSynchronousTask则是主线程执行。
可以看到只有Synchronous以后主线程是会等AsyncTask里面的逻辑执行完了之后才会继续往下走。而使用Background主线程不会阻塞。
既然StartSynchronousTask会阻塞主线程,那我用AsyncTask的意义何在呢?直接一开始就单线程不就完事了?
问得好,这个方法即使是在ue4源码里用到的地方也极少。我认为这个方法的意义在于给AsyncTask多了一点灵活性,当我们在使用多线程时发现部分逻辑代码只能跑在主线程或者它跑异步线程其实并没有变快,这个时候想把它改成单线程的时候就很方便。
总结
AsyncTask系统实现的多线程与你自己字节继承FRunnable实现的原理相似,还可以利用UE4提供的线程池。当使用多线程不满意时也可以调用StartSynchronousTask改成主线程执行。