为什么要使用多线程,如果你是想找UE4的多线程资料,肯定不是一个小白了,所以多线程的好处就不用我告诉你了。
如果你想去感受一些多线程的好处,或者想去学习一个多线程的案例。建议,去看看这个博主的文章。
利用多线程执多任务的几种方式
FRunnable
- UE4 内置的FRunnable和FRunnableThread。FRunnable是线程的执行体,提供了相应的接口。FRunnableThread代表是线程的本身,该类会派生出平台相关的子类,win32下对应的是FRunnableThreadWin,建议读者看看某个平台下的具体实现。在创建时需要指定一个FRunnable,用于线程执行。使用这个可以自定义创建线程,执行任务。
用 ue4 创建一个c++项目。
FRunnable 示例
代码示例:
用c++ 创建一个UObject,然后继承FRunnable.
MyRunnableObject.h
#pragma once
#include "CoreMinimal.h"
#include "HAL/Runnable.h"
#include "Windows/WindowsPlatformProcess.h"
class MULTITHREAD_API MyRunnableObject : public FRunnable
{
public:
MyRunnableObject();
~MyRunnableObject();
int runTime = 0;
bool bIsRunning = false;
virtual bool Init()
{
bIsRunning = true;
UE_LOG(LogTemp, Display, TEXT("Init."));
return true;
}
virtual uint32 Run()
{
while (bIsRunning) {
UE_LOG(LogTemp, Display, TEXT("Run %d."), runTime);
++runTime;
FPlatformProcess::Sleep(1);
}
return 0;
}
virtual void Stop()
{
UE_LOG(LogTemp, Display, TEXT("kill is called via FRunnableThread,stop now."));
bIsRunning = false;
}
virtual void Exit()
{
UE_LOG(LogTemp, Display, TEXT("Run finished"));
}
};
在项目的c++的gamemode 里边引用这个UObject,代码实现如下:
multiThreadGameModeBase.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "MyRunnableObject.h"
#include "Engine/EngineTypes.h"
#include "multiThreadGameModeBase.generated.h"
UCLASS()
class MULTITHREAD_API AmultiThreadGameModeBase : public AGameModeBase
{
GENERATED_BODY()
virtual void BeginPlay();
MyRunnableObject* myRunnableObject;
FTimerHandle timeHandle;
FRunnableThread* myThread;
UFUNCTION()
void OnTimer();
};
multiThreadGameModeBase.cpp
#include "multiThreadGameModeBase.h"
#include "HAL/RunnableThread.h"
#include "GameFramework/Actor.h"
void AmultiThreadGameModeBase::BeginPlay()
{
Super::BeginPlay();
myRunnableObject = new MyRunnableObject();
myThread = FRunnableThread::Create(myRunnableObject, TEXT("myRunnableObject"));
GetWorldTimerManager().SetTimer(timeHandle, this, &AmultiThreadGameModeBase::OnTimer, 5.0f);
}
void AmultiThreadGameModeBase::OnTimer()
{
if (myThread)
{
myThread->Kill(true);
delete myRunnableObject;
delete myThread;
}
}
这个demo 主要演示了,创建了一个自定义的线程执行体,线程里执行的业务逻辑放到了run() 方法中。然后再GameMode 中创建这个自定义的线程,并且5秒之后,销毁这个线程的一个示例。
原理
这种方式本质就是创造了一个继承自FRunnable的类,把这个类要执行的任务分配给其他的线程去执行。在实现多线程的时候,我们需要将FRunnbale作为参数传递到真正的线程里面,然后才能通过线程调用FRunnable的Run 方法。所谓真正的线程其实就是FRunnableThread,不同平台的线程都继承自他,如FRunnableThreadWin,里面会调用Windows平台的创建线程的API接口。
FAsyncTask
- 利用ue4 内置的线程池,通过使用异步任务系统,实现多线程执行任务。
这个案例在网上比较多的就是查找第N个质数。
基于第三人称模板,创建一个c++示例。
代码示例如下:
PrimeCalculator.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PrimeCalculator.generated.h"
UCLASS()
class THREAD_API APrimeCalculator : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
APrimeCalculator();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
public:
UFUNCTION(BlueprintCallable)
void RunPrimeTask(int32 num_primes);
UFUNCTION(BlueprintCallable)
void RunPrimeTaskOnMain(int32 num_primes);
};
// =============================================
class PrimeSearchTask : public FNonAbandonableTask
{
public:
int32 prime_count;
public:
PrimeSearchTask(int32 _prime_count);
~PrimeSearchTask();
// required by UE4, is required
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(PrimeSearchTask, STATGROUP_ThreadPoolAsyncTasks);
}
void DoWork();
void DoWorkMain();
};
PrimeCalculator.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "PrimeCalculator.h"
// Sets default values
APrimeCalculator::APrimeCalculator()
{
// 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 APrimeCalculator::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void APrimeCalculator::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void APrimeCalculator::RunPrimeTask(int32 num_primes)
{
(new FAutoDeleteAsyncTask<PrimeSearchTask>(num_primes))->StartBackgroundTask();
}
void APrimeCalculator::RunPrimeTaskOnMain(int32 num_primes)
{
PrimeSearchTask* task = new PrimeSearchTask(num_primes);
task->DoWorkMain();
delete task;
}
PrimeSearchTask::PrimeSearchTask(int32 _prime_count)
{
prime_count = _prime_count;
}
PrimeSearchTask::~PrimeSearchTask()
{
UE_LOG(LogTemp, Warning, TEXT("Task Finished!!!"));
}
void PrimeSearchTask::DoWork()
{
int primes_found = 0;
int current_Test_number = 2;
while (primes_found < prime_count)
{
bool is_prime = true;
for (int i=2;i<current_Test_number/2;i++)
{
if (current_Test_number % i == 0)
{
is_prime = false;
break;
}
}
if(is_prime)
{
primes_found++;
if (primes_found % 1000 == 0)
{
UE_LOG(LogTemp, Warning, TEXT("primes found: %i"), primes_found);
}
}
current_Test_number++;
}
}
void PrimeSearchTask::DoWorkMain()
{
DoWork();
}
基于这个PrimeCalculator创建一个蓝图子类,PrimeCalculator_BP
截图如下:
该案例说明,如果我们把一个数据量很大的算法拿到主线程上执行,就是执行RunPrimeTaskOnMain。主线程会卡,就是说,我们的画面会卡住。如果把它放到其他线程去执行的话,就不会出现画面卡顿的现象。
FQueuedThreadPool
FQueuedThreadPool: 虚基类,定义线程池常用的接口。FQueueThreadPoolBase 继承FQueuedThreadPool,实现具体的方法。FQueueThreadPoolBase 有三个比较重要的TArray,TArray<IQueuedWork*> QueuedWork(要被执行的任务),TArray<FQueuedThread*> QueuedThreads(空闲线程),TArray<FQueuedThread*> AllThreads(所有线程)。线程池里面维护了多个线程FQueuedThread与多个任务称为IQueuedWork。
在线程池里面所有的线程都是FQueuedThread类型,只是更简单的说FQueuedThread是继承自FRunnable的线程执行体,每个FQueuedThread里面包含一个FRunnableThread作为内部成员。
Asyntask与IQueuedWork
线程池的任务IQueuedWork本身是一个接口,所以得有具体实现。这里你就应该应该能猜到,所谓的AsynTask其实就是对IQueuedWork的具体实现。这里AsynTask泛指FAsyncTask与FAutoDeleteAsyncTask两个类。
FAsyncTask有几个特点:
- FAsyncTask是一个模板类,真正的AsyncTask需要你自己写。通过DoWork提供你要执行的具体任务,然后把你的类作为模板参数传递过去
- 使用FAsyncTask就可以使用UE提供的线程池FQueuedThreadPool。
- 在执行FAsyncTask任务时,如果你在执行StartBackgroundTask的时候会使用GThreadPool线程池,当然你也可以在参数里面指定自己创建的线程池
- 创建FAsyncTask并不一定要使用新的线程,您可以调用函数StartSynchronousTask直接在当前线程上执行任务。
FAutoDeleteAsyncTask与FAsyncTask是相似的,但是有一些差异,
- 默认使用UE提供的线程池FQueuedThreadPool,无法使用其他线程池
- FAutoDeleteAsyncTask在任务完成后会通过线程池的Destroy函数删除自身或者在执行DoWork后删除自身,而FAsyncTask需要手动删除
总的来说,AsyncTask系统实现的多线程与你自己的字节继承FRunnable实现的原理相似性,不过他在用法上比较简单,而且还可以直接借用UE4提供的线程池,很方便。
最后,不要在非GameThread线程内部执行以下几个操作:
- 不要产生/修改/删除UObject或AActors
- 不要使用定时器TimerManager
- 不要使用任何布局接口,例如DrawDebugLine
一开始我也不是很理解,所以就在其他线程里面执行了Spawn操作,然后就蹦在了下面的地方。可以看到,SpawnActor的时候会执行物理数据的初始化,而这个操作是必须要在主线程里面执行的,我猜其他的位置肯定还有很多类似的宏。至于原因,我想就是我们最前面提到的“游戏不适合利用多线程优化”,游戏游戏中的各个部分非常依赖顺序,多再者,游戏逻辑如此复杂,你怎么做到避免“竞争条件”呢?到处加锁么?我想那样的话,游戏代码就没法看了吧。
TaskGraph
- 第三种就是TaskGraph系统,这个比较复杂,我会另开一个写这个系统的使用。
《Exploring in UE4》多线程机制详解[原理分析] 地址
Ue4 多线程系统 地址
Unreal Engine 4 —— 多线程任务构建 地址
UE4多线程任务系统详解地址