UE4_多線程、異步執行任務

爲什麼要使用多線程,如果你是想找UE4的多線程資料,肯定不是一個小白了,所以多線程的好處就不用我告訴你了。
如果你想去感受一些多線程的好處,或者想去學習一個多線程的案例。建議,去看看這個博主的文章。

利用多線程執多任務的幾種方式

FRunnable

  1. 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

  1. 利用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有幾個特點:

  1. FAsyncTask是一個模板類,真正的AsyncTask需要你自己寫。通過DoWork提供你要執行的具體任務,然後把你的類作爲模板參數傳遞過去
  2. 使用FAsyncTask就可以使用UE提供的線程池FQueuedThreadPool。
  3. 在執行FAsyncTask任務時,如果你在執行StartBackgroundTask的時候會使用GThreadPool線程池,當然你也可以在參數裏面指定自己創建的線程池
  4. 創建FAsyncTask並不一定要使用新的線程,您可以調用函數StartSynchronousTask直接在當前線程上執行任務。

FAutoDeleteAsyncTask與FAsyncTask是相似的,但是有一些差異,

  1. 默認使用UE提供的線程池FQueuedThreadPool,無法使用其他線程池
  2. FAutoDeleteAsyncTask在任務完成後會通過線程池的Destroy函數刪除自身或者在執行DoWork後刪除自身,而FAsyncTask需要手動刪除

總的來說,AsyncTask系統實現的多線程與你自己的字節繼承FRunnable實現的原理相似性,不過他在用法上比較簡單,而且還可以直接借用UE4提供的線程池,很方便。
最後,不要在非GameThread線程內部執行以下幾個操作:

  1. 不要產生/修改/刪除UObject或AActors
  2. 不要使用定時器TimerManager
  3. 不要使用任何佈局接口,例如DrawDebugLine

一開始我也不是很理解,所以就在其他線程裏面執行了Spawn操作,然後就蹦在了下面的地方。可以看到,SpawnActor的時候會執行物理數據的初始化,而這個操作是必須要在主線程裏面執行的,我猜其他的位置肯定還有很多類似的宏。至於原因,我想就是我們最前面提到的“遊戲不適合利用多線程優化”,遊戲遊戲中的各個部分非常依賴順序,多再者,遊戲邏輯如此複雜,你怎麼做到避免“競爭條件”呢?到處加鎖麼?我想那樣的話,遊戲代碼就沒法看了吧。

TaskGraph

  1. 第三種就是TaskGraph系統,這個比較複雜,我會另開一個寫這個系統的使用。

《Exploring in UE4》多線程機制詳解[原理分析] 地址
Ue4 多線程系統 地址
Unreal Engine 4 —— 多線程任務構建 地址
UE4多線程任務系統詳解地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章