爲什麼要使用多線程,如果你是想找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多線程任務系統詳解地址