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多线程任务系统详解地址

  • 6
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
在 Unreal Engine 5 中,你可以使用 C++ 来实现异步线程执行。下面是一个简单的示例代码: ```cpp #include "Async/Async.h" // 定义一个异步任务 class MyAsyncTask : public FNonAbandonableTask { public: // 构造函数传入需要执行的参数 MyAsyncTask(int32 InParam) : Param(InParam) {} // 在异步线程中执行任务 void DoWork() { // 执行一些耗时操作,比如计算或者加载资源等等 // ... // 任务执行结束后可以将结果回调到主线程 FGraphEventRef GameThreadTask = FFunctionGraphTask::CreateAndDispatchWhenReady([&]() { // 在主线程中执行回调操作,可以更新UI或者其他逻辑 // ... }, TStatId(), nullptr, ENamedThreads::GameThread); } // 返回任务名称 static const TCHAR* Name() { return TEXT("MyAsyncTask"); } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(MyAsyncTask, STATGROUP_ThreadPoolAsyncTasks); } private: int32 Param; }; // 启动异步任务 void StartAsyncTask(int32 Param) { // 创建异步任务并提交到线程池中执行 MyAsyncTask* AsyncTask = new MyAsyncTask(Param); AsyncTask->StartBackgroundTask(); } ``` 在上面的示例代码中,我们定义了一个名为 `MyAsyncTask` 的异步任务,它继承自 `FNonAbandonableTask`,并实现了 `DoWork` 方法来执行异步线程中的任务。在 `DoWork` 方法中,你可以执行一些耗时操作,并在任务结束后通过 `FFunctionGraphTask` 将结果回调到主线程进行处理。 要启动异步任务,你可以调用 `StartAsyncTask` 函数,并传入需要执行的参数。该函数会创建一个 `MyAsyncTask` 实例,并提交到线程池中执行。 请注意,异步任务执行是在一个单独的线程中进行的,因此你需要确保在任务中不会访问或修改与主线程共享的对象或数据,以避免潜在的竞态条件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值