背景

本篇为UE多线程的使用与底层代码实现的问题。多线程是优化项目性能的重要方式之一。

在系统组工作过程中,听到最多的是优化是对于Flush的禁止工作。防止Loading资源的时候占用资源,这是一个庞大且精细的过程,在顶层编码的时候也需要去注意某些Sync操作是否会引发Flush。Flush这种同步加载的堵塞操作,需要去分析Flush的操作处理。故有此篇作为基础篇,里面涉及到部分的os的基础概念的实现,后续可以去复习一下os的pv操作部分。

本章梳理一下对UE多线程的顶层调用处理。

2025.11.06 关于FQueuedThreadPool 和TaskGraph 后续再研究,埋个坑。

引擎的线程

在创建自己的多线程之前,需要先了解UE本身已经运行在多个线程上了,最主要的是:

  • 游戏线程(Game Thread):
    • 这是最主要的线程,大部分的蓝图和C++游戏逻辑都在这里执行。
    • 负责处理玩家输入、Actor的Tick、蓝图逻辑、物理模拟的触发(但计算本身不在主线程)、游戏状态更新等。 当这个线程的任务太重,游戏就会卡顿,帧率(FPS)下降。
  • 渲染线程(Render Thread)
    • 负责接收游戏线程传来的渲染指令(称为”渲染命令”) ,并将其提交给GPU执行。
    • 它和游戏线程是并行工作的。游戏线程在准备下一帧的数据时,渲染线程正在提交上一帧的渲染命令。它们之间通过一个”渲染命令队列”进行通信。
  • RHI线程(Rendering Hardware Interface Thread):
    • 在部分平台和配置下,渲染命令会进一步从一个渲染线程分发到RHI线程,再由RHI线程与GPU驱动通信,进一步减轻渲染线程的负担。
    • RHI线程通过配置来控制是否开启。

多线程工具

对各个平台线程实现进行了封装,抽象出了FRunnable,引擎中大部分都是继承与这个类经常处理。

FRunnable

这种方式的核心包含三个结构,分别是FRunnable、FRunnableThread以及FThreadManager

Runnable

  • new一个FRunnable子对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "HAL/Runnable.h"

class MyRunnable : public FRunnable {
public:
virtual bool Init() override; // 初始化 runnable 对象
virtual uint32 Run() override; // 运行 runnable 对象
virtual void Stop() override; // 停止 runnable 对象,线程提前终止时被调用
virtual void Exit() override; // 退出 runnable 对象
};

bool MyRunnable::Init() { return true; }
uint32 MyRunnable::Run() { return 0; }
void MyRunnable::Stop() {}
void MyRunnable::Exit() {}

图片

FRunnableThread

  • 创建线程

创建调用FRunnableThread::Create 这是一个静态工厂方法,用于完成FRunnableThread的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
------------------------RunnableThread.h----------------------

#include "HAL/RunnableThread.h"

static FRunnableThread * Create
(
class FRunnable * InRunnable, // Runnable 对象
const TCHAR * ThreadName, // 线程名称
uint32 InStackSize, // 线程栈大小,0表示使用当前线程的栈大小
EThreadPriority InThreadPri, // 线程优先级
uint64 InThreadAffinityMask
);

// 返回值:若成功则返回创建的线程,否则返回 nullptr

------------------------------------------------------------------------------
#include "HAL/RunnableThread.h"

FRunnable * Runnable = new MyRunnable();
FRunnableThread* RunnableThread = FRunnableThread::Create(Runnable, TEXT("LaLaLaDeMaXiYa!"));

对于FRunnableThread的工作有:

  • 调用各个平台内部的 API 创建线程
  • 调用可执行体的 Init()、Run()、Exit()
  • 提供管理线程生命周期的各种方法
  • 兼容不支持多线程的平台:不过这个得在实现自定义 FRunnable 的时候,同时继承 FSingleThreadRunnable 并重载 Tick() 方法,使用 Tick 来调用可执行体

FThreadManager

通过FRunnableThread 创建的线程是通过 FThreadManager 进行统一管理

CreateInternal根据平台的不同实现不同,常用平台中,Android和iOS都是采用的 pthread标准线程库,Windows平台是单独实现的。线程创建完毕后会统一调用

1
FThreadManager::Get().AddThread(ThreadID, this);

线程本身添加至管理器。如 WindowsRunnableThread.h

FRunnableThreadWin::CreateInternal 函数。标准线程对象 FRunnableThreadPThread 则是在入口点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
virtual PthreadEntryPoint GetThreadEntryPoint() {
return _ThreadProc;
}

static void *STDCALL _ThreadProc(void *pThis) {
check(pThis);
FRunnableThreadPThread* ThisThread = (FRunnableThreadPThread*)pThis;
// cache the thread ID for this thread (defined by the platform)
ThisThread->ThreadID = FPlatformTLS::GetCurrentThreadId();
// ====================>>这里将线程本身加入管理器 <<==========================
FThreadManager::Get().AddThread(ThisThread->ThreadID, ThisThread);
// set the affinity. This function sets affinity on the current thread, so don't call in the Create function which will trash the main thread affinity.
FPlatformProcess::SetThreadAffinityMask(ThisThread->ThreadAffinityMask);
// run the thread!
ThisThread->PreRun();
ThisThread->Run();
ThisThread->PostRun();
pthread_exit(NULL);
return NULL;
}

有以下API:

FThreadManager::AddThread

FThreadManager::RemoveThread

FThreadManager::ForEachThread

FThreadManager::Tick

  • 对fake thread及其对应的runnable objects进行tick
  • 在FEngineLoop::Tick方法中被调用

AsyncTask

这是一种基于线程池(利用ue4底层的线程池机制来调度任务)的异步任务处理系统,这套系统同样是基于Runnable实现的,在实际工作中,我们经常会遇到需要将部分代码放在特定的线程中进行执行

注:Andriod多线程开发里面也会用到AsyncTask,二者的实现原理非常相似。

这个方法调用GraphTask创建了一个立刻执行的任务,可以看成是是TaskGraph的简单版本。
这个需要执行的任务可以指定执行的线程,需要注意的是,如果某个任务从AnyThread改成GameThread执行,AsyncTask下面的代码也是不会阻塞的。这个时候还是单线程,只是传入的Lambda方法会在主线程一帧里的其他地方调用,GameThread执行由于没有线程切换,因此整体时间消耗会少于AnyThread,不过问题则在于加长了整体的单帧时间消耗。

FQueuedThreadPool

UE里面的线程池,FQueuedThreadPool。和一般的线程池实现类似,线程池里面维护了多个线程FQueuedThread与多个任务队列IQueuedWork,线程是按照队列的方式来排列的。在引擎PreInit的时候执行相关的初始化操作,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// FEngineLoop.PreInit   LaunchEngineLoop.cpp
if (FPlatformProcess::SupportsMultithreading())
{
{
GThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfWorkerThreadsToSpawn();

// we are only going to give dedicated servers one pool thread
if (FPlatformProperties::IsServerOnly())
{
NumThreadsInThreadPool = 1;
}
verify(GThreadPool->Create(NumThreadsInThreadPool, 128 * 1024));
}
#ifUSE_NEW_ASYNC_IO
{
GIOThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfIOWorkerThreadsToSpawn();
if (FPlatformProperties::IsServerOnly())
{
NumThreadsInThreadPool = 2;
}
verify(GIOThreadPool->Create(NumThreadsInThreadPool, 16 * 1024, TPri_AboveNormal));
}
#endif// USE_NEW_ASYNC_IO

#ifWITH_EDITOR
// when we are in the editor we like to do things like build lighting and such
// this thread pool can be used for those purposes
GLargeThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInLargeThreadPool = FMath::Max(FPlatformMisc::NumberOfCoresIncludingHyperthreads() - 2, 2);

verify(GLargeThreadPool->Create(NumThreadsInLargeThreadPool, 128 * 1024));
#endif
}

img

使用AsyncTask

创建一个FTestAsycTask类继承与FNonAbandonableTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 示例:FTestAsyncTask
// 这是一个继承自 FNonAbandonableTask 的异步任务类。
// 当线程池被销毁时,此类任务不会像普通任务那样被丢弃(Abandon),而是会等待其执行完毕。
class FTestAsyncTask : public FNonAbandonableTask
{
// 声明友元类,以便 FAutoDeleteAsyncTask 能够管理本任务的生命周期
friend class FAutoDeleteAsyncTask<FTestAsyncTask>;

int32 TargetCounter; // 目标任务计数器
double OldTime; // 记录旧时间,可用于耗时计算

public:
/**
* 构造函数
* @param Target 目标计数,通常由任务创建者传入,代表任务需要处理的量或目标
*/
FTestAsyncTask(int32 Target)
: TargetCounter(Target)
, OldTime(0) // 显式初始化 OldTime
{
}

/**
* 核心工作任务 (必须重写)
* 此函数将在工作线程中被调用,包含需要异步执行的具体逻辑。
*/
void DoWork()
{
// 在这里实现具体的异步任务逻辑。
for (int32 i = 0; i < TargetCounter; ++i)
{
// 模拟工作负载,每次循环睡眠一小段时间
FPlatformProcess::Sleep(0.01f);

// 可以在这里更新进度或处理数据...
}

// 任务完成,可以输出日志等
UE_LOG(LogTemp, Log, TEXT("FTestAsyncTask::DoWork() 完成。处理了 %d 个计数。"), TargetCounter);
}

/**
* 获取统计ID (必须重写)
* 用于UE的性能分析系统(Stats)来追踪此任务的执行情况。
* @return 此任务的统计标识符
*/
FORCEINLINE TStatId GetStatId() const
{
// 使用QUICK_DECLARE_CYCLE_STAT宏快速声明一个循环统计对象
RETURN_QUICK_DECLARE_CYCLE_STAT(FTestAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
}

// 注意:由于使用了 FAutoDeleteAsyncTask,任务对象在执行完毕后会自动删除。
// 因此,除非有特殊资源管理需求,否则通常不需要自定义析构函数。
};

// 如何使用这个任务:
// 启动一个后台自动删除的任务:
// (new FAutoDeleteAsyncTask<FTestAsyncTask>(1000))->StartBackgroundTask();
  • 为什么要继承FNonAbandonableTask?

当线程池被销毁的时候,会调用Abandon函数。继承FNonAbandonableTask的话这个时候就不会丢弃而且等待执行完。如果需要丢弃则不继承,并且自己实现CanAbandonAbandon函数。源码里可丢弃的任务参考:FAsyncStatsFile

  • StartBackgroundTaskStartSynchronousTask

    StartBackgroundTask会利用线程池里空闲的线程来执行。

    StartSynchronousTask则是主线程执行。

    只有Synchronous以后主线程是会等AsyncTask里面的逻辑执行完了之后才会继续往下走。而使用Background主线程不会阻塞。

  • AsyncTask系统实现的多线程与你自己字节继承FRunnable实现的原理相似,还可以利用UE4提供的线程池。当使用多线程不满意时也可以调用StartSynchronousTask改成主线程执行。

Async

Async除了上述几种方式,还有AsyncTask、Async、AsyncThread、AsyncPool等几个全局方法

核心:Async:通用异步执行。/AsyncTask:使用TaskGraph执行异步操作。/AsyncThread:使用单独的线程执行异步操作。/AsyncPool:使用线程池执行异步操作。

Async全局方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#include "TestAsyncActor.h"
#include "TestAsyncTask.h" // 包含自定义的异步任务类FTestAsyncTask

// 构造函数:设置Actor的默认属性
ATestAsyncActor::ATestAsyncActor()
{
// 设置此Actor每帧都调用Tick(),如果不需要可以提高性能关闭
PrimaryActorTick.bCanEverTick = true;
}

// 使用FAutoDeleteAsyncTask在后台线程执行异步任务
void ATestAsyncActor::TestAsyncTaskClass()
{
OldTime = FPlatformTime::Seconds(); // 记录任务开始时间

// 创建FAutoDeleteAsyncTask并启动后台任务(自动删除,无需手动管理内存)[7](@ref)
// StartBackgroundTask会在其他线程中执行此任务[7](@ref)
(new FAutoDeleteAsyncTask<FTestAsyncTask>(TargetCounter))->StartBackgroundTask();

double End = FPlatformTime::Seconds();
// 记录主线程结束时间(注意:主线程不会等待异步任务完成)
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}

// 使用FAutoDeleteAsyncTask在当前线程同步执行任务
void ATestAsyncActor::TestAsyncTaskClass_Synchronous()
{
OldTime = FPlatformTime::Seconds(); // 记录任务开始时间

// 创建FAutoDeleteAsyncTask并同步执行(在当前线程立即执行)[7](@ref)
// StartSynchronousTask会在当前线程中马上执行此任务[7](@ref)
(new FAutoDeleteAsyncTask<FTestAsyncTask>(TargetCounter))->StartSynchronousTask();

double End = FPlatformTime::Seconds();
// 记录主线程结束时间(同步执行会阻塞直到任务完成)
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}

// 使用AsyncTask在任意线程执行Lambda表达式
void ATestAsyncActor::TestAsyncTaskFunc_AnyThread()
{
OldTime = FPlatformTime::Seconds(); // 记录任务开始时间

// 使用AsyncTask在任意可用线程执行Lambda任务[8](@ref)
AsyncTask(ENamedThreads::AnyThread, [=]() // ENamedThreads::AnyThread表示任务可在任何线程执行
{
double Result = 0;
// 模拟耗时计算:计算平方根累加
for (int32 i = 0; i < TargetCounter; i++)
{
float j = i;
Result += FMath::Sqrt(j) / TargetCounter;
}

// 输出计算结果
UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, Result);

double End = FPlatformTime::Seconds();
// 记录异步任务完成时间
UE_LOG(LogTemp, Log, TEXT("@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
});

double End = FPlatformTime::Seconds();
// 记录主线程结束时间(立即返回,不等待异步任务)
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}

// 使用AsyncTask在游戏线程执行Lambda表达式
void ATestAsyncActor::TestAsyncTaskFunc_GameThread()
{
OldTime = FPlatformTime::Seconds(); // 记录任务开始时间

// 使用AsyncTask在游戏线程执行Lambda任务[8](@ref)
// ENamedThreads::GameThread确保任务在游戏主线程执行(适合UI操作)
AsyncTask(ENamedThreads::GameThread, [=]()
{
double Result = 0;
// 模拟耗时计算
for (int32 i = 0; i < TargetCounter; i++)
{
float j = i;
Result += FMath::Sqrt(j) / TargetCounter;
}
// 输出计算结果
UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, Result);

double End = FPlatformTime::Seconds();
// 记录任务完成时间
UE_LOG(LogTemp, Log, TEXT("@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
});

double End = FPlatformTime::Seconds();
// 记录主线程结束时间
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}

// 使用Async函数在任务图中执行无返回值的异步操作
void ATestAsyncActor::TestAsyncFunc_NoReturn()
{
// 注释说明:TaskGraph主线程计算时间最慢,但主线程结束时间最慢
OldTime = FPlatformTime::Seconds(); // 记录开始时间

// 使用Async函数,EAsyncExecution::TaskGraph表示使用任务图系统执行[3](@ref)
Async(EAsyncExecution::TaskGraph, [=]()
{
double Result = 0;
// 模拟耗时计算
for (int32 i = 0; i < TargetCounter; i++)
{
float j = i;
Result += FMath::Sqrt(j) / TargetCounter;
}
// 输出计算结果
UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, Result);

double End = FPlatformTime::Seconds();
// 记录异步任务完成时间
UE_LOG(LogTemp, Log, TEXT("@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
});

double End = FPlatformTime::Seconds();
// 记录主线程结束时间
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}

// 使用Async函数执行有返回值的异步操作
void ATestAsyncActor::TestAsyncFunc_WithReturn()
{
OldTime = FPlatformTime::Seconds(); // 记录开始时间

// 使用Async执行有返回值的任务,返回TFuture<double>类型[3](@ref)
TFuture<double> FutureResult = Async(EAsyncExecution::TaskGraph, [=]()
{
double Result = 0;
// 模拟耗时计算
for (int32 i = 0; i < TargetCounter; i++)
{
float j = i;
Result += FMath::Sqrt(j) / TargetCounter;
}

double End = FPlatformTime::Seconds();
// 记录异步任务完成时间
UE_LOG(LogTemp, Log, TEXT("@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
return Result; // 返回计算结果
});

// 使用Get()获取返回值,这会阻塞主线程直到异步任务完成[3](@ref)
UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, FutureResult.Get());

double End = FPlatformTime::Seconds();
// 记录主线程结束时间(会包含等待异步任务完成的时间)
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}

// 使用ParallelFor进行并行计算(案例中总计算时间可能会变慢)
void ATestAsyncActor::TestAsyncFunc_ParallelFor()
{
OldTime = FPlatformTime::Seconds(); // 记录开始时间
int Internal = TargetCounter / 10; // 将总任务分成10份

// 使用Async包装ParallelFor进行并行计算[3](@ref)
auto FutureResult = Async(EAsyncExecution::TaskGraph, [=]()
{
TArray<double> ResultArray;
ResultArray.Init(0, 10); // 初始化结果数组

// 使用ParallelFor并行执行循环[3](@ref)
ParallelFor(ResultArray.Num(), [&ResultArray,Internal,this](int32 Index)
{
// 每个并行任务处理一部分数据
for (int32 i = Index * Internal; i < (Index + 1) * Internal; i++)
{
float j = i;
ResultArray[Index] += FMath::Sqrt(j)/TargetCounter;
}
});

// 汇总所有并行任务的结果
double Sum = 0;
for (int32 j = 0; j < ResultArray.Num(); j++)
{
Sum += ResultArray[j];
}

UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, Sum);
double End = FPlatformTime::Seconds();
UE_LOG(LogTemp, Log, TEXT("@%u wait millisecond(%f) end."), __LINE__, (End-OldTime)*1000);
return Sum; // 返回汇总结果
});

// 注释掉的代码:如果取消注释会阻塞主线程等待结果
// UE_LOG(LogTemp, Log, TEXT("@%u wait TargetCounterR(%f)"), __LINE__, FutureResult.Get());

double End = FPlatformTime::Seconds();
// 记录主线程结束时间
UE_LOG(LogTemp, Log, TEXT(__FUNCTION__"@%u wait millisecond(%f) main thread end."), __LINE__, (End-OldTime)*1000);
}

TaskGraph

通过TaskGraph系统来异步完成一些自定义任务

这是一套抽象的异步任务处理系统,通过这套系统,我们可以创建多个多线程任务,并且指定各个任务之间的依赖关系,并按照该关系来依次处理任务,所有任务依赖关系形成一张有向无环图。

askGraph任务图,是用来解决多线程中任务需要先后执行顺序的问题

我们以游戏开发中工作流为例:

  • 首先是策划提出需求案子
  • 然后美术设计概念图
    • 模型师根据概念图建模
    • 动画师等建模完成后制作动画
  • 程序在案子提出后开发特性
  • 等上面全部完成后策划进行验收

img

这是最简单的情况,现在程序特性比较复杂,将分给三个程序员分别开发(即子任务)。

img

使用

先创建两个任务,一个表示工作内容FWorkTask,一个表示汇报FReportTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include "Async/TaskGraphInterfaces.h"

void TaskGraphExample()
{
// 任务 A
FGraphEventRef TaskA = FFunctionGraphTask::CreateAndDispatchWhenReady([]()
{
HeavyStageA();
}, TStatId(), nullptr, ENamedThreads::AnyBackgroundHiPriTask);

// 任务 B
FGraphEventRef TaskB = FFunctionGraphTask::CreateAndDispatchWhenReady([]()
{
HeavyStageB();
}, TStatId(), nullptr, ENamedThreads::AnyBackgroundHiPriTask);

// 任务 C 依赖 A 和 B
TArray<FGraphEventRef> Prereqs{ TaskA, TaskB };
FGraphEventRef TaskC = FFunctionGraphTask::CreateAndDispatchWhenReady([]()
{
MergeResults();
}, TStatId(), &Prereqs, ENamedThreads::AnyBackgroundThreadNormalTask);

}
-----------------------------------------------
○简单案例2,自定义类
Plain Text
// 定义一个简单的任务类
class FMySimpleTask
{
public:
FMySimpleTask(int32 InValue) : Value(InValue) {}

// 必需的方法
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FMySimpleTask, STATGROUP_TaskGraphTasks);
}

static FORCEINLINE ENamedThreads::Type GetDesiredThread()
{
return ENamedThreads::AnyThread;
}

static FORCEINLINE ESubsequentsMode::Type GetSubsequentsMode()
{
return ESubsequentsMode::TrackSubsequents;
}

// 任务执行函数
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
// 执行实际工作
Result = Value * Value;
UE_LOG(LogTemp, Log, TEXT("Task completed: %d^2 = %d"), Value, Result);
}

int32 GetResult() const { return Result; }

private:
int32 Value;
int32 Result = 0;
};

// ====================================================================================
// 创建并执行任务
void ExecuteSimpleTask()
{
FGraphEventRef TaskEvent = TGraphTask<FMySimpleTask>::CreateTask()
.ConstructAndDispatchWhenReady(666); // 666是构造FMySimpleTask传入的参数InValue

// 可以选择等待完成
FTaskGraphInterface::Get().WaitUntilTaskCompletes(TaskEvent);
}

多线程同步

std::atomic 原子机制

Atomic operations(原子操作) 保证CPU在读取和写入内存时总线操作是不可分割的。它是许多高级同步机制的基础,主要优势是可以进行比较快的进行比较和解锁操作。一个用Atomics实现的样例如下:

1
2
3
4
5
6
7
8
class FThreadSafeCounter{
public:
int32 Add( int32 Amount ) {
return FPlatformAtomics::InterlockedAdd(&Counter, Amount);
}
private:
volatile int32 Counter; // 因为值可能以编译器无法预测的异步方式被改变,声明为volatile禁用优化
};

Locking 锁机制

UE提供了四种不同的锁:

  • FCriticalSection:临界区
    • 根据各个平台的互斥锁进行的抽象
      • Windows 平台是基于Windows平台的临界区
      • iOS, Android,Linux平台则是使用的POSIX的线程标准实现
  • FSpinLock:自旋锁
  • FScopeLock:区域锁,基于作用域的锁,在构造时对当前区域加锁,离开作用域时执行析构并解锁,类似class有
    • FScopedMovementUpdate
    • FScopeCycleCounter
    • FScopedEvent
  • FRWLock:读写锁

Signaling 信号机制

类型为FSemaphore,是一种信号量与互斥锁类型,这种类型包含了一种信号机制,但不是所有平台都支持。更加常用的线程间通信机制是 FEvent

Waiting and FEvent

这是一种等待同步机制,包含如下的一些事件类型:

  • FEvent:阻塞直至被触发或者超时,经常被用来激活其他工作线程
  • FScopedEvent:这是对FEvent的一次包装,阻塞在域代码退出时

other

使用的一些tips:

  • UE4常见的容器类【TArray, TMap, TSet】通常都不是线程安全的,需要我们仔细编写代码保证线程安全

常见的线程安全类有:

  • FThreadSafeCounter计数器
  • FThreadSingleton 单例类
  • FThreadIdleStats 线程空闲状态统计类
  • TLockFreePointerList 无锁队列
  • TQueue队列

总结

多线程同步

同步工具 核心用途 适用场景
FCriticalSection / FScopeLock 互斥访问(一次一个线程) 通用共享数据保护
FRWLock / FRWScopeLock 读写分离(多读一写) 读多写少的配置、资源表
FEvent 线程间通知和等待 生产者-消费者、任务开始/完成信号
FSpinLock 极短临界区的忙等待锁 高性能场景下的微秒级操作保护
std::atomic 轻量级的不可分割操作 计数器、状态标志

避免在GameTread上调用Wait()或者EnsureCompletion()之类的阻塞函数。使用RAII(FScopeLock, FRWScopeLock)是避免死锁的有效手段,尽量避免手动调用lock/unlock。

临界区的设置不要太大或太小,太大会产生不必要的开销,太小可能无法起到保护作用FQueuedThreadPool。

工具

工具 适用场景 复杂度 使用度
Async/AsyncTask/AsyncThread/AsyncPool 简单的、一次性的异步任务,配合回调。 简单 最常用
FAsyncTask 需要定义明确任务类的短时可重用的计算 中等 常用
ParallelFor 并行化处理大型数组或数据集合。 简单 常用于数据并行
FRunnable 长时间运行、有独立生命周期的后台服务。 较复杂 有特定需求时使用
TaskGraph 复杂的、有依赖关系的并行任务链。 复杂 主要用于有复杂依赖的需求时

1.面对平行计算大量数据(计算密集型),且相互之间没有依赖的时候考虑使用ParallelFor。当需要快速实现一个简单的,一次性的异步操作,例如一次I/O操作,优先从Async/AsyncTask/AsyncThread/AsyncPool里面选择。

2.任务逻辑复杂,需要良好封装,或者打算多次复用的,考虑AsyncTask。

3.如果有复杂前置任务依赖的场景,使用TaskGraph。

4.如果是长时间持有运行的,需要自己管理生命周期的,通过自定义FRunnable实现。

从1到4,依次优先考虑。

类图

img

参考

UE4 C++基础 - 多线程

【UE】 UE 多线程框架 - 简书

《Exploring in UE4》多线程机制详解

UE 多线程案例-CSDN博客

【UE·引擎篇】Runnable、TaskGraph、AsyncTask、Async多线程开发指南 - 知乎

5.1 进程、线程基础知识 | 小林coding | Java面试学习