【UE5 C++课程系列笔记】22——多线程基础——FRunnable和FRunnableThread

目录

1、FRunnable

[1.1 概念](#1.1 概念)

[1.2 主要成员函数](#1.2 主要成员函数)

[(1)Init 函数](#(1)Init 函数)

[(2)Run 函数](#(2)Run 函数)

[(3)Stop 函数](#(3)Stop 函数)

[(4)Exit 函数](#(4)Exit 函数)

2、FRunnableThread

[2.1 概念](#2.1 概念)

[2.2 主要操作](#2.2 主要操作)

(1)创建线程

(2)停止线程

[3、 使用示例](#3、 使用示例)

(1)解决线程竞争方式1:加锁

(2)解决线程竞争方式2:线程安全布尔类型FThreadSafeBool

案例源代码


1、FRunnable

1.1 概念

FRunnable 是一个抽象基类,定义了线程执行任务的基本接口和生命周期相关的函数,它提供了一种规范,让开发者可以基于这个抽象类派生出自己的类,并重写其中的方法来定义具体在线程中要执行的任务以及处理线程的启动、停止、暂停等生命周期相关操作。通过实现 FRunnable 接口,虚幻引擎能够以统一的方式管理和调度这些自定义的可运行任务,方便地将复杂的计算、耗时操作等放到单独的线程中执行,提高程序的并发性能和响应速度,同时避免阻塞主线程影响用户体验。

1.2 主要成员函数

(1)Init 函数

函数原型:virtual bool Init();

作用:该函数在线程启动时被调用,用于进行一些初始化操作,例如初始化线程中需要使用的资源(如打开文件、创建数据库连接、初始化数据结构等)。如果初始化成功,函数应该返回 true;如果初始化过程中出现错误,导致线程无法正常启动运行,应该返回 false,此时线程将不会继续执行后续的操作。

示例代码:

cpp 复制代码
class MyRunnable : public FRunnable
{
public:
    virtual bool Init() override
    {
        // 初始化一些资源,比如创建一个数组等
        MyDataArray.Empty();
        MyDataArray.Add(10);
        MyDataArray.Add(20);
        return true;
    }
};

(2)Run 函数

函数原型:virtual uint32 Run();

作用:这是线程执行主体任务的函数,在线程启动并且 Init 函数成功执行后被调用。开发者需要在这个函数中编写具体的线程执行逻辑,例如进行复杂的数学计算、数据处理、网络通信等耗时操作。函数返回值通常表示线程的退出码(一般返回 0 表示正常退出,其他非零值可以根据具体需求定义为不同的错误码等情况)。

示例代码:

cpp 复制代码
class MyRunnable : public FRunnable
{
public:
    virtual uint32 Run() override
    {
        for (int i = 0; i < 1000; ++i)
        {
            // 模拟一些耗时的计算操作,比如累加计算
            Result += i;
            FPlatformProcess::Sleep(0.001);  // 模拟每次计算间隔一点时间,避免 CPU 占用过高
        }
        return 0;
    }
private:
    int Result = 0;
};

(3)Stop 函数

函数原型:virtual void Stop();

作用:当线程需要停止时(比如外部调用了停止线程的相关函数,或者线程执行完了预设的任务等情况),这个函数会被调用,用于进行一些清理资源的操作,例如关闭文件句柄、释放动态分配的内存、断开网络连接等,确保线程在结束时不会遗留未处理的资源,避免造成内存泄漏或其他资源相关的问题。

示例代码:

cpp 复制代码
class MyRunnable : public FRunnable
{
public:
    virtual void Stop() override
    {
        // 释放之前在 Init 或者其他地方动态分配的内存
        if (MyAllocatedMemory!= nullptr)
        {
            FMemory::Free(MyAllocatedMemory);
            MyAllocatedMemory = nullptr;
        }
    }
private:
    void* MyAllocatedMemory = nullptr;
};

(4)Exit 函数

函数原型:virtual void Exit();

作用:这个函数在整个线程生命周期结束后被调用,主要用于一些最后的清理或者记录线程执行相关信息等操作,相对来说它的使用场景比 Stop 函数更偏向于整个线程完全结束后的一些收尾工作,不过在很多简单的实现中,可能和 Stop 函数的功能有一定重叠,具体取决于开发者的需求和实际的业务逻辑。

cpp 复制代码
class MyRunnable : public FRunnable
{
public:
    virtual void Exit() override
    {
        // 记录线程执行的一些统计信息,比如执行时间等
        FDateTime EndTime = FDateTime::Now();
        double ExecutionTime = (EndTime - StartTime).GetTotalSeconds();
        UE_LOG(LogTemp, Log, TEXT("线程执行时间为: %f 秒"), ExecutionTime);
    }
private:
    FDateTime StartTime;
};

2、FRunnableThread

2.1 概念

FRunnableThread 是用于实际创建和管理线程的类,它提供了与操作系统底层线程创建机制的接口,能够基于不同的操作系统(如 Windows、Linux、macOS 等)创建对应的线程,并将 FRunnable 派生类中定义的任务(也就是 Run 函数里的内容)放到所创建的线程中去执行。它屏蔽了不同操作系统线程创建和管理的差异,让开发者可以用统一的方式在虚幻引擎中使用多线程功能,并且可以对线程进行一些基本的控制,比如启动、暂停、恢复、停止等操作。

2.2 主要操作

(1)创建线程

在如下示例代码中,首先创建了 MyCustomRunnable 类的实例对象 runnable,这个类继承自 FRunnable 并定义了线程的具体任务等逻辑。然后通过 FRunnableThread::Create 函数创建了一个名为 "MyCustomThread"、优先级为正常的线程,将 runnable 对象传递进去,使得这个线程启动后会执行 MyCustomRunnable 类中 Run 函数里定义的任务内容。

cpp 复制代码
MyCustomRunnable* runnable = new MyCustomRunnable();
FRunnableThread* thread = FRunnableThread::Create(runnable, TEXT("MyCustomThread"), TPri_Normal);

(2)停止线程

要停止线程的运行,可以调用停止线程的相关函数,这会触发 FRunnable 派生类中的 Stop 函数被调用,然后线程会正常结束执行,释放相关资源并退出。

cpp 复制代码
thread->Stop();  

3、 使用示例

通过结合前面学到的子系统、共享指针和本节的多线程内容,实现在线程中循环输出打印信息的功能。

  1. 首先新建一个空白C++ 类,这里命名为"ThreadSubsystem"

让类"ThreadSubsystem"继承"UGameInstanceSubsystem",并添加必要的反射信息

重写父类"UGameInstanceSubsystem"的"ShouldCreateSubsystem"、"Initialize"、"Deinitialize"方法。其中"ShouldCreateSubsystem"用于询问是否应该实际创建该子系统,"Initialize"用于进行各种初始化操作,"Deinitialize"用于在子系统生命周期结束,需要被销毁时进行清理操作。

添加创建线程和回收线程的方法,这里分别命名为"InitSimpleThread"和"ReleaseSimpleThread"

在创建子系统时创建线程,在子系统销毁时回收线程。

  1. 再新建一个空白C++ 类,这里命名为"SimpleRunnable"

让"SimpleRunnable"类继承"FRunnable",然后重写父类的"Init"、"Run"、"Exit"、"Stop"函数

定义一个FString类型的线程名称SimpleRunnableThreadName,默认值为None,再通过构造函数初始化成员变量 SimpleRunnableThreadName

由于SimpleRunnableThreadName是受保护的,因此再定义一个方法"GetThreadName"用于获取线程名称。

  1. 在"ThreadSubsystem"中引入"SimpleRunnable",然后定义一个TSharedPtr智能指针类型的成员变量SimpleRunnable,用于管理 FSimpleRunnable 类的实例对象。通过使用智能指针,可以方便地对 FSimpleRunnable 实例的生命周期进行管理,避免手动处理内存释放等问题,同时在多线程环境下也能更好地保证对象的有效性和安全性。当多个地方需要访问和使用FSimpleRunnable实例时,智能指针可确保只有在所有引用都释放后,实例对象才会被真正销毁,有效地防止了内存泄漏和悬空指针等问题,并且在创建、赋值、传递等操作上更加便捷和安全。

再声明了一个名为 SimpleThreadPtr 的指针变量,类型为 FRunnableThread

实现方法"InitSimpleThread"、"ReleaseSimpleThread"如下。

在第25行代码中,使用 MakeShared 函数创建了一个 FSimpleRunnable 类型的实例,并通过智能指针 SimpleRunnable来管理实例的生命周期。在创建 FSimpleRunnable 实例时,传入字符串Thread000作为线程名称。

在第26行代码中,调用 FRunnableThread::Create 函数来创建实际的线程对象。第一个参数 SimpleRunnable.Get() 用于获取 SimpleRunnable 智能指针所指向的 FSimpleRunnable 实例对象的地址,使得创建的线程知道要执行的具体任务逻辑是由这个 FSimpleRunnable 实例定义的(也会执行 FSimpleRunnable 类中重写的 Run 函数里的内容)。第二个参数 *SimpleRunnable->GetThreadName() 是获取前面创建的 FSimpleRunnable 实例中设置的线程名称。最终,将 FRunnableThread::Create 函数返回的指向新创建的线程对象的指针赋值给 SimpleThreadPtr,后续就可以通过这个指针来操作该线程(比如启动、暂停、停止等操作)

在"ReleaseSimpleThread"函数中首先通过 SimpleRunnable.IsValid() 检查 SimpleRunnable 智能指针所指向的 FSimpleRunnable 实例是否有效。如果有效,就调用该实例的 Stop 函数。再通过判断 SimpleThreadPtr 是否为非空指针来确定之前创建的线程对象是否存在。如果存在,就调用 SimpleThreadPtr->WaitForCompletion() 函数,该函数的作用通常是让当前调用线程阻塞等待,直到被 SimpleThreadPtr 指向的线程执行完成(也就是线程执行完了 FSimpleRunnable 类中 Run 函数里定义的任务逻辑,并且执行了相关的停止和清理操作后正常退出)。这样做可以确保在后续对线程相关资源进行进一步清理或者对整个子系统进行状态更新时,线程已经完全结束,避免出现数据不一致或者资源访问冲突等问题。

  1. 在"SimpleRunnable"类中重写Run函数如下。通过while循环确保线程一直执行,再调用FPlatformProcess::Sleep(0.5)函数,让线程暂停 0.5 秒,使得 CPU 有时间去处理其他线程或者主线程的任务。
  1. 编译后,新建一个Actor类,这里命名为"BP_ThreadActor",在事件图表中调用"ThreadSubsystem"的"InitSimpleThread"和"ReleaseSimpleThread"方法

运行后可以看到一直在打印日志

但是当想结束运行时发现点击结束按钮后卡住。

下面开始改进,首先定义一个打印信息的方法"PrintWaring",其中AsyncTask 是虚幻引擎提供的一个用于在指定线程中异步执行任务的函数,这里传入了 ENamedThreads::GameThread 表示要将打印任务放到游戏线程中去执行。

再定义一个打印线程信息的方法"PrintThreadInfo"

需要引入"ThreadManager"

定义一个布尔变量bRunning ,只有bRunning 为true的时候循环才继续执行,否则退出循环并结束线程。

  1. 重新编译后,在关卡蓝图中设置通过1、2键控制线程的启动和停止

运行结果如下,可以看到线程成功启动,执行一段时间后,成功被手动停止。

但是此时还会存在线程竞争问题。这是因为Run函数和Stop函数的执行不在同一个线程,在多线程环境下,对于 bRunning 变量的访问存在并发访问冲突的风险。因为 Run 函数中的循环依赖于 bRunning 的值来判断是否继续执行,而 Stop 函数会修改 bRunning 的值,如果多个线程同时访问或者修改这个变量(比如一个线程尝试通过 Stop 函数停止线程,而另一个线程正在 Run 函数中读取 bRunning 的值来判断循环条件),就可能导致线程的行为出现异常,比如线程无法正常停止或者出现意外的提前停止等情况。

为了解决线程竞争问题,这里提供两种解决方法。

(1)解决线程竞争方式1:加锁

这里我们通过加"临界区"解决这个问题。先定义一个临界区。

FScopeLock Lock(&CriticalSection)创建一个 FScopeLock 类型的对象 Lock,并将其与一个 FCriticalSection 类型的临界区对象 CriticalSection 相关联。

FScopeLock 类的设计遵循了 RAII 原则,在其构造函数中,会获取传入的 FCriticalSection 对象所代表的临界区锁,使得从构造函数执行完开始,对应的临界区就处于被锁定状态,其他线程若试图访问同一个临界区保护的代码或资源时,就会被阻塞,直到当前持有锁的线程释放该锁为止。当 FScopeLock 对象的生命周期结束(也就是离开其所在的代码块范围时,例如函数执行到对应的代码块末尾,或者遇到 return 语句等情况导致代码块结束),它的析构函数会自动被调用,在析构函数中,会释放之前在构造函数里获取的临界区锁,将临界区的访问权限开放给其他线程,使得其他线程有机会获取锁并访问受保护的资源。

此时可以看到无论我们开关多少次线程也不会存在安全风险

(2)解决线程竞争方式2:线程安全布尔类型FThreadSafeBool

引入所需库

bRunning 变量的类型由普通的bool类型改为FThreadSafeBool类型

此时不再需要加锁

案例源代码

"SimpleRunnable.h"

cpp 复制代码
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "HAL/Runnable.h"
#include "HAL/ThreadManager.h"


class STUDY_API FSimpleRunnable : public FRunnable
{
public:
	FSimpleRunnable(FString InThreadName) :SimpleRunnableThreadName(InThreadName) {};
	~FSimpleRunnable() {};

	virtual bool Init() override;
	virtual uint32 Run() override;
	virtual void Exit() override;
	virtual void Stop() override;

	FString GetThreadName();

	static void PrintWaring(FString InStr);

protected:

	void PrintThreadInfo();

	FString SimpleRunnableThreadName = TEXT("None");

	bool bRunning = true;  //用于判断线程是否继续执行

	FCriticalSection CriticalSection;
};

"SimpleRunnable.cpp"

cpp 复制代码
// Fill out your copyright notice in the Description page of Project Settings.


#include "Thread/SimpleRunnable.h"

bool FSimpleRunnable::Init()
{
	PrintWaring(TEXT("Thread Init"));
	PrintThreadInfo();
	return true;
}

uint32 FSimpleRunnable::Run()
{
	PrintWaring(TEXT("Thread Run"));
	PrintThreadInfo();

	while (bRunning)
	{
		FPlatformProcess::Sleep(0.5);
		FScopeLock Lock(&CriticalSection);
		if (bRunning == false)
		{
			break;
		}
		PrintWaring(TEXT("Thread Working"));
	}

	return uint32();
}

void FSimpleRunnable::Exit()
{
	PrintWaring(TEXT("Thread Exit"));
	PrintThreadInfo();
}

void FSimpleRunnable::Stop()
{
	FScopeLock Lock(&CriticalSection);
	bRunning = false;
	PrintWaring(TEXT("Thread Stop"));
	PrintThreadInfo();
}

FString FSimpleRunnable::GetThreadName()
{
	return SimpleRunnableThreadName;
}

void FSimpleRunnable::PrintWaring(FString InStr)
{
	AsyncTask(ENamedThreads::GameThread, [InStr]() {
		UE_LOG(LogTemp, Warning, TEXT("ThreadLog:[%s]"), *InStr);
	});
}

void FSimpleRunnable::PrintThreadInfo()
{
	uint32 CurrentID = FPlatformTLS::GetCurrentThreadId();
	FString CurrentThread = FThreadManager::Get().GetThreadName(CurrentID);
	FString Info = FString::Printf(TEXT("--CurrentThreadID:[%d]--CurrentThreadName:[%s]"), CurrentID, *CurrentThread);
	PrintWaring(Info);
}

"ThreadSubsystem.h"

cpp 复制代码
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "SimpleRunnable.h"
#include "ThreadSubsystem.generated.h"

UCLASS()
class STUDY_API UThreadSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	virtual void Deinitialize() override;

public:
	UFUNCTION(BlueprintCallable)
	void InitSimpleThread();
	UFUNCTION(BlueprintCallable)
	void ReleaseSimpleThread();

protected:
	TSharedPtr<FSimpleRunnable> SimpleRunnable;

	FRunnableThread* SimpleThreadPtr = nullptr;

};

"ThreadSubsystem.cpp"

cpp 复制代码
// Fill out your copyright notice in the Description page of Project Settings.


#include "Thread/ThreadSubsystem.h"

bool UThreadSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
    return true;
}

void UThreadSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
    //InitSimpleThread();
}

void UThreadSubsystem::Deinitialize()
{
    //ReleaseSimpleThread();
    Super::Deinitialize();
}

void UThreadSubsystem::InitSimpleThread()
{
    SimpleRunnable = MakeShared<FSimpleRunnable>(TEXT("Thread000"));
    SimpleThreadPtr = FRunnableThread::Create(SimpleRunnable.Get(), *SimpleRunnable->GetThreadName());
}

void UThreadSubsystem::ReleaseSimpleThread()
{
    if (SimpleRunnable.IsValid())
    {
        SimpleRunnable->Stop();
    }

    if (SimpleThreadPtr)
    {
        SimpleThreadPtr->WaitForCompletion();
    }
}
相关推荐
windwind200015 小时前
UE5 打包要点
游戏·ue5
ue星空17 小时前
UE播放声音
ue5·声音
ue星空1 天前
UE5AI感知组件
ue5
Zhichao_971 天前
【UE5 C++课程系列笔记】21——弱指针的简单使用
笔记·ue5
流行易逝3 天前
7.UE5横板2D游戏,添加分类,创建攻击,死亡逻辑,黑板实现追击玩家行为
游戏·ue5
ue星空3 天前
UE5行为树浅析
人工智能·ai·ue5·行为树
ue星空5 天前
UE5失真材质
ue5·材质
ue星空5 天前
UE5材质节点Distance
ue5·材质
ue星空6 天前
UE5动画蓝图
ue5·蓝图