【UE4 C++ 基础知识】<13> 多线程——TaskGraph
概述
- TaskGraph 系统是UE4一套抽象的异步任务处理系统
- TaskGraph 可以看作一种”基于任务的并行编程“设计思想下的实现
- 通过TaskGraph ,可以创建任意多线程任务, 异步任务, 序列任务, 并行任务等,并可以指定任务顺序, 设置任务间的依赖, 最终形成一个任务图, 该系统按照设定好的依赖关系来分配任务图中的任务到各个线程中执行, 最终执行完整个任务图。
- TaskGraph适合简单的任务或者想实现有依赖关系的线程,复杂的任务推荐使用 Runnable 或者 AsynTask
TaskGraph 类定义
模块构成
自定义的任务必须要满足 TGraphTask 中对 Task 的接口需求
构造函数可以传参,最好不要使用引用类型,会有”悬空引用“的风险,可以使用指针来代替引用
GetStatId()固定写法,函数内传入自定义TaskGraph类型GetDesiredThread()指定在哪个线程运行ENamedThreads::TypeAnyThreadGameThread适合访问UObject,可能会阻塞主线程RHIThreadAudioThread
- 也可以通过
FAutoConsoleTaskPriority对象获取合适的线程
GetSubsequentsMode()后续执行模式,因为可以有子任务ESubsequentsMode::TrackSubsequents存在后续任务,实际没有后续任务也不影响,常用该类型ESubsequentsMode::FireAndForget没有后续任务
DoTask()线程逻辑执行函数
TaskGraph 简单实现
FTaskGraph_SimpleTask 任务类
class FTaskGraph_SimpleTask
{
FString m_ThreadName; public:
FTaskGraph_SimpleTask(const FString& ThreadName) : m_ThreadName(ThreadName) {}
~FTaskGraph_SimpleTask(){} // 固定写法
FORCEINLINE TStatId GetStatId() const {
RETURN_QUICK_DECLARE_CYCLE_STAT(FTaskGraph_SimpleTask, STATGROUP_TaskGraphTasks);
} // 指定在哪个线程运行
static ENamedThreads::Type GetDesiredThread() { return ENamedThreads::AnyThread; } // 后续执行模式
static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; } // 线程逻辑执行函数
void DoTask(ENamedThreads::TypeCurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
// 逻辑任务
// 可创建 Child Task
UE_LOG(LogTemp, Warning, TEXT("Thread %s Begin!"), *m_ThreadName);
UE_LOG(LogTemp, Warning, TEXT("Thread %s End!"), *m_ThreadName);
}
};
ATaskGraphActor 调用的AActor
UFUNCTION(BlueprintCallable)
void CreateTaskGraph_SimpleTask(const FString& ThreadName);
void ATaskGraphActor::CreateTaskGraph_SimpleTask(const FString& ThreadName)
{
TGraphTask<FTaskGraph_SimpleTask>::CreateTask().ConstructAndDispatchWhenReady(ThreadName); // ThreadName 为 FTaskGraph_SimpleTask 构造函数参数
}

了解任务的创建
在上一小节中,我们可以使用以下代码创建任务
FGraphEventRef GraphEventRef = TGraphTask<FTaskGraph_SimpleTask>::CreateTask().ConstructAndDispatchWhenReady(ThreadName); // 创建任务立即执行
TGraphTask<FTaskGraph_SimpleTask>* GraphTask = TGraphTask<FTaskGraph_SimpleTask>::CreateTask().ConstructAndHold(ThreadName); // 创建任务挂起,等待 unlock() 触发任务执行
TGraphTask<T>是模板类,可以指定自定义任务的类型CreateTask()- 第一个参数
Prerequisites- 用来指定该任务依赖的事件数组,默认为 NULL.
- 在所有依赖事件都触发后,该任务才会放到任务队列里面分配给线程执行。
- 第二个参数
ENamedThreads::Type- 用来指定线程类型
// 完整函数为
static FConstructor CreateTask(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
- 第一个参数
ConstructAndDispatchWhenReady()- 创建任务后立即执行
- 调用创建的TaskGraph类型(本例中为FTaskGraph_SimpleTask)的构造函数
- 可以传递构造函数的参数,进行初始化
ConstructAndHold()- 创建任务后挂起,等待 unlock() 唤醒任务执行
- 参数同上
TaskGraph 并行任务
具有依赖关系的并行任务
- 如图所示,B 依赖于 A0 和 A1,即 A0、A1 执行完毕后 B 才开始执行
- C0 和 C1 依赖于B 的完成


派发任务
- 如图所示,虚线框为派发子任务
- B 的开始依赖于 A0 和 A1 的完成
- B 有两个子任务 B0 和 B1,B 的完成 需要满足 B0 和 B1 也完成
- C0 和 C1 的开始依赖于B 的完成,因而 C0 和 C1 的开始 需要满足 B、B0 和 B1 都完成


C++代码
- TaskGraph_SimpleTask.h
#pragma once
#include "CoreMinimal.h"
#include "TaskGraph_SimpleTask.generated.h"
DECLARE_DELEGATE_OneParam(FGraphTaskDelegate,const FString&); // 单播委托
USTRUCT(BlueprintType)
struct FTaskGraphItem { // 结构体用来传参
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(BlueprintReadWrite)
FString m_ThreadName; // 线程名称
FGraphEventRef m_GraphEventRef; // 自动执行的任务
TGraphTask<class FTaskGraphWithPrerequisitesAndChild>* m_GraphTask; // 需要触发执行的任务
// 构造函数
FTaskGraphItem(FString ThreadName = TEXT("None"), FGraphEventRef GraphEventRef = nullptr, TGraphTask<class FTaskGraphWithPrerequisitesAndChild>* GraphTask = nullptr)
:m_ThreadName(ThreadName), m_GraphEventRef(GraphEventRef), m_GraphTask(GraphTask) {}
~FTaskGraphItem()
{
m_GraphEventRef = nullptr;
m_GraphTask = nullptr;
}
};
class FTaskGraph_SimpleTask // 作为具体执行的任务
{
FString m_ThreadName;
FGraphTaskDelegate m_GraphTaskDelegate;
public:
FTaskGraph_SimpleTask(const FString& ThreadName, FGraphTaskDelegate GraphTaskDelegate)
: m_ThreadName(ThreadName), m_GraphTaskDelegate(GraphTaskDelegate) {}
~FTaskGraph_SimpleTask(){}
// 固定写法
FORCEINLINE TStatId GetStatId() const {
RETURN_QUICK_DECLARE_CYCLE_STAT(FTaskGraph_SimpleTask, STATGROUP_TaskGraphTasks);
}
// 指定在主线程,因为用到 AActor 蓝图里的函数
static ENamedThreads::Type GetDesiredThread() { return ENamedThreads::GameThread; }
// 后续执行模式
static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; }
// 线程逻辑执行函数
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
check(IsInGameThread()); //确认是否在主线程
FString message = FString::Printf(TEXT("SimpleTaskTask[%s] execute the GraphTaskDelegate!"), *m_ThreadName);
m_GraphTaskDelegate.ExecuteIfBound(message);
//UE_LOG(LogTemp, Warning, TEXT("SimpleTaskTask[%s] execute!"), *m_ThreadName);
}
};
class FTaskGraphWithPrerequisitesAndChild // 作为通用任务,可作为依赖事件的任务,也可作为子任务
{
FString m_ThreadName;
TArray<TGraphTask<FTaskGraphWithPrerequisitesAndChild>*> m_ChildGraphTask; // 子任务数组
FGraphTaskDelegate m_GraphTaskDelegate; // 单播委托
public:
// 构造函数
FTaskGraphWithPrerequisitesAndChild(const FString& ThreadName, const TArray<TGraphTask<FTaskGraphWithPrerequisitesAndChild>*>& ChildTask, FGraphTaskDelegate GraphTaskDelegate)
: m_ThreadName(ThreadName), m_ChildGraphTask(ChildTask), m_GraphTaskDelegate(GraphTaskDelegate) {}
~FTaskGraphWithPrerequisitesAndChild() {}
// 固定写法
FORCEINLINE TStatId GetStatId() const {
RETURN_QUICK_DECLARE_CYCLE_STAT(FTaskGraphWithPrerequisitesAndChild, STATGROUP_TaskGraphTasks);
}
// 指定在哪个线程运行
static ENamedThreads::Type GetDesiredThread() { return ENamedThreads::AnyThread; }
// 后续执行模式
static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; }
// 线程逻辑执行函数
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
UE_LOG(LogTemp, Warning, TEXT("Task[%s] Begin!"), *m_ThreadName);
// 执行子任务,此处通用任务作为子任务
if (m_ChildGraphTask.Num()>0)
{
for (auto GraphTaskItem : m_ChildGraphTask)
{
GraphTaskItem->Unlock(); // 唤醒子任务
MyCompletionGraphEvent->DontCompleteUntil(GraphTaskItem->GetCompletionEvent());
}
// 如有需要,可设法检测所有子任务是否都完成
}
// 创建并执行子任务,本处作为具体执行的任务
MyCompletionGraphEvent->DontCompleteUntil(TGraphTask<FTaskGraph_SimpleTask>::CreateTask().ConstructAndDispatchWhenReady(m_ThreadName, m_GraphTaskDelegate));
UE_LOG(LogTemp, Warning, TEXT("Task[%s] End!"), *m_ThreadName);
}
};
- TaskGraphActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TaskGraph_SimpleTask.h"
#include "TaskGraphActor.generated.h"
UCLASS()
class TIPS_API ATaskGraphActor : public AActor
{
GENERATED_BODY()
public:
ATaskGraphActor();
protected:
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// 创建任务
UFUNCTION(BlueprintCallable)
FTaskGraphItem CreateGraphTask(const FString& ThreadName, const TArray<FTaskGraphItem>& Prerequisites,const TArray<FTaskGraphItem>& ChildTasks,bool DispatchWhenReady );
// 创建任务,CreateGraphTask 的简化
UFUNCTION(BlueprintCallable)
FTaskGraphItem CreateGraphTaskPure(const FString& ThreadName, bool DispatchWhenReady) {
return CreateGraphTask(ThreadName, TArray<FTaskGraphItem>(), TArray<FTaskGraphItem>(), DispatchWhenReady);
}
// 唤醒挂起的任务
UFUNCTION(BlueprintCallable)
void TriggerGraphTask(FTaskGraphItem TaskGraphItem);
// 用于任务中执行的回调函数
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
void OnTaskFinished(const FString& message);
}
- TaskGraphActor.cpp
FTaskGraphItem ATaskGraphActor::CreateGraphTask(const FString& ThreadName, const TArray<FTaskGraphItem>& Prerequisites, const TArray<FTaskGraphItem>& ChildTasks, bool DispatchWhenReady)
{
FGraphEventArray PrerequisiteEvents; // 依赖事件
TArray<TGraphTask<FTaskGraphWithPrerequisitesAndChild>*> ChildGraphTask; // 子任务
UE_LOG(LogTemp, Warning, TEXT("Task[%s] is Created!"), *ThreadName);
if (Prerequisites.Num()>0)
{
for (FTaskGraphItem item : Prerequisites) // 结构体数组提取依赖事件
{
if (item.m_GraphEventRef)
{
PrerequisiteEvents.Add(item.m_GraphEventRef);
UE_LOG(LogTemp, Warning, TEXT("Task[%s] wait Task[%s]!"), *ThreadName,*item.m_ThreadName);
}
else if (item.m_GraphTask)
{
PrerequisiteEvents.Add(item.m_GraphTask->GetCompletionEvent());
UE_LOG(LogTemp, Warning, TEXT("Task[%s] wait Task[%s]!"), *ThreadName,*item.m_ThreadName);
}
}
}
if (ChildTasks.Num()>0)
{
for (FTaskGraphItem item : ChildTasks) // 提取子任务
{
if (item.m_GraphTask)
{
ChildGraphTask.Add(item.m_GraphTask);
UE_LOG(LogTemp, Warning, TEXT("Task[%s] is Task[%s] child task!"), *item.m_ThreadName, *ThreadName);
}
}
}
FGraphTaskDelegate GraphTaskDelegate = FGraphTaskDelegate::CreateUObject(this, &ATaskGraphActor::OnTaskFinished);
if (DispatchWhenReady)
{ // 创建立即执行的任务,返回结构体参数
return FTaskGraphItem(ThreadName, TGraphTask<FTaskGraphWithPrerequisitesAndChild>::CreateTask(&PrerequisiteEvents).ConstructAndDispatchWhenReady(ThreadName, ChildGraphTask, GraphTaskDelegate));
}
// 创建任务后挂起,等待触发,返回结构体参数
return FTaskGraphItem(ThreadName, nullptr, TGraphTask<FTaskGraphWithPrerequisitesAndChild>::CreateTask(&PrerequisiteEvents).ConstructAndHold(ThreadName, ChildGraphTask, GraphTaskDelegate));
}
void ATaskGraphActor::TriggerGraphTask(FTaskGraphItem TaskGraphItem)
{
if (TaskGraphItem.m_GraphTask)
{
TaskGraphItem.m_GraphTask->Unlock();
UE_LOG(LogTemp, Warning, TEXT("Task %s Trigger!"), *TaskGraphItem.m_ThreadName);
}
}
扩展
ParallelFor
- 基于 TaskGraph
- 多次调用函数体,可以做简单的遍历处理
- bForceSingleThread 设置单线程还是多线程
ParallelFor
(
int32 Num,
TFunctionRef < void )> Body,
bool bForceSingleThread,
bool bPumpRenderingThread
)
void ParallelForWithPreWork
(
int32 Num,
TFunctionRef < void )> Body,
TFunctionRef < void ()> CurrentThreadWorkToDoBeforeHelping,
bool bForceSingleThread,
bool bPumpRenderingThread
)
ParallelFor(100, [](int32 CurrIdx) {
int32 Sum = 0;
for (int32 Idx = 0; Idx < CurrIdx * 100; ++Idx)
Sum += FMath::Sqrt(1234.56f);
});
AsyncTask
- 本质上使用 TaskGraph
//异步执行一个Function 函数指针
void AsyncTask(ENamedThreads::Type Thread, TUniqueFunction<void()> Function)
{
TGraphTask<FAsyncGraphTask>::CreateTask().ConstructAndDispatchWhenReady(Thread, MoveTemp(Function));
}
//异步执行一个 Lambda 表达式
void AsyncTask(ENamedThreads::Type Thread, [&](){} );
参考
- Legacy/Multi-Threading: Task Graph System
- 虚幻4与现代C++:基于任务的并行编程与TaskGraph入门
- 虚幻4与现代C++:使用TaskGraph实现Fork-Join模型
- UE4 C++进阶07 异步操作-基于TaskGraph的多线程
- 虚幻4 Task Graph System 介绍
- ParallelFor
- ParallelForWithPreWork
【UE4 C++ 基础知识】<13> 多线程——TaskGraph的更多相关文章
- 【UE4 C++ 基础知识】<11>资源的同步加载与异步加载
同步加载 同步加载会造成进程阻塞. FObjectFinder / FClassFinder 在构造函数加载 ConstructorHelpers::FObjectFinder Constructor ...
- 【UE4 C++ 基础知识】<12> 多线程——FRunnable
概述 UE4里,提供的多线程的方法: 继承 FRunnable 接口创建单个线程 创建 AsyncTask 调用线程池里面空闲的线程 通过 TaskGraph 系统来异步完成一些自定义任务 支持原生的 ...
- 【UE4 C++ 基础知识】<3> 基本数据类型、字符串处理及转换
基本数据类型 TCHAR TCHAR就是UE4通过对char和wchar_t的封装 char ANSI编码 wchar_t 宽字符的Unicode编码 使用 TEXT() 宏包裹作为字面值 TCHAR ...
- 【UE4 C++ 基础知识】<14> 多线程——AsyncTask
概念 AsyncTask AsyncTask 系统是一套基于线程池的异步任务处理系统.每创建一个AsyncTas,都会被加入到线程池中进行执行 AsyncTask 泛指 FAsyncTask 和 FA ...
- JAVA基础知识之多线程——三种实现多线程的方法及区别
所有JAVA线程都必须是Thread或其子类的实例. 继承Thread类创建线程 步骤如下, 定义Thead子类并实现run()方法,run()是线程执行体 创建此子类实例对象,即创建了线程对象 调用 ...
- java基础知识总结--多线程
1.扩展Java.lang.Thread类 1.1.进程和线程的区别: 进程:每个进程都有自己独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1~n个线程. 线程:同一类线 ...
- 【UE4 C++ 基础知识】<8> Delegate 委托
概念 定义 UE4中的delegate(委托)常用于解耦不同对象之间的关联:委托的触发者不与监听者有直接关联,两者通过委托对象间接地建立联系. 监听者通过将响应函数绑定到委托上,使得委托触发时立即收到 ...
- JAVA基础知识之多线程——线程组和未处理异常
线程组 Java中的ThreadGroup类表示线程组,在创建新线程时,可以通过构造函数Thread(group...)来指定线程组. 线程组具有以下特征 如果没有显式指定线程组,则新线程属于默认线程 ...
- JAVA基础知识之多线程——控制线程
join线程 在某个线程中调用其他线程的join()方法,就会使当前线程进入阻塞状态,直到被join线程执行完为止.join方法类似于wait, 通常会在主线程中调用别的线程的join方法,这样可以保 ...
随机推荐
- awk 命令-随笔
awk语法: awk [option] 'pattern{action}' file ... awk [参数] '条件{动作}' 文件 ... 解析: 命令: awk 参数: -F "&qu ...
- ubuntu-常用设置备忘
环境 系统平台:Ubuntu 16.04.6 其他版本设置也差不多 vim喜好设置 系统安装vim sudo apt-get install vim 通过修改 /etc/vim/vimrc 文件设置 ...
- openswan协商流程之(七):main_inR3
主模式第六包(收包):main_inR3 1. 序言 main_inR3()函数是ISAKMP协商过程中第一阶段的最后一个报文的接收处理函数,它的作用同main_inI3_outR3()部分功能相同: ...
- IPv4掩码与掩码位数的转换
1. 根据掩码获取掩码的位数 int mask2len(unsigned int mask) { /*eg: 255.255.255.0 255.0.255.255.0*/ int bit=0,len ...
- Vue设置全局js/css样式
''' 配置全局js mian.js: import settings from '@/assets/js/settings' Vue.prototype.$settings = settings; ...
- centos7关于防火墙的一些操作
防火墙相关 # 检查防火墙状态 systemctl status firewalld # 开启防火墙 systemctl start firewalld # 关闭防火墙 systemctl stop ...
- IO流实现简单的文件的剪切
思路: 判断 即将 复制的文件是文件夹还是文件 遍历需要复制的源文件夹 如果是文件夹,就通过流创建一个同样的子文件夹 如果是文件,就复制过去 接下来上代码 public class Demo1 { p ...
- 有备无患「GitHub 热点速览 v.21.38」
作者:HelloGitHub-小鱼干 数据库最重要的一个功能是容灾备份,备份不只是对数据库重要,对日常工作生活的我们一样重要,比如花了一个工作日写的代码没有备份(虽然可能只有 1 行-)总归是一个让人 ...
- PHP中的日期相关函数(一)
日期相关的操作函数是我们在日常的工作开发中最常接触到的功能.当然,大部分同学可能最多用到的就是 date() . time() 这两个函数,我们今天先不讲这两个函数,或许后面的文章也不太会讲它们,毕竟 ...
- Shell系列(12)- 预定义变量(5)
预定义变量 作用 $? 常用:最后一次执行的命令的返回状态. 如果这个变量的值为0,证明上一个命令正确执行:如果这个变量的值为非0(具体是哪个数,由命令自己来决定),则证明上一个命令执行不正确了 $$ ...