UE5 任务系统 TaskSystem
TaskSystem 简介
UE5 引入了一个新的任务系统(TaskSystem),它是对底层 TaskGraph 系统的高层封装。这个新系统提供了更简单、更直观的 API,使得异步任务的管理变得更加容易。TaskSystem 的目标是简化常见的异步编程模式,并提供更细粒度的任务控制。
任务优先级
任务系统支持两种优先级系统,允许开发者根据任务的重要性调整其执行顺序:
基本优先级 (
ETaskPriority
): 提供两种通用的优先级。cppenum class ETaskPriority { Normal, // 普通优先级 (默认) High, // 高优先级 };
扩展优先级 (
EExtendedTaskPriority
): 提供更精细的控制,可以将任务绑定到特定的游戏引擎线程。cppenum class EExtendedTaskPriority { None, // 无特殊优先级 (默认) Inline, // 内联执行:在当前线程同步执行。注意在某些上下文中,例如在被 Pipe 管理的任务中,内联任务依然遵循 FIFO 顺序。 TaskEvent, // 任务事件优先级:可能与任务事件的触发相关联。 // 游戏线程相关 GameThreadNormalPri, // 绑定到游戏线程,普通优先级 GameThreadHiPri, // 绑定到游戏线程,高优先级 // 渲染线程相关 RenderThreadNormalPri, // 绑定到渲染线程,普通优先级 RenderThreadHiPri, // 绑定到渲染线程,高优先级 // RHI线程相关 RHIThreadNormalPri, // 绑定到 RHI 线程,普通优先级 RHIThreadHiPri, // 绑定到 RHI 线程,高优先级 };
使用扩展优先级可以将特定任务绑定到如游戏线程、渲染线程或 RHI 线程执行,这对于需要访问特定线程上下文的数据或资源的任务非常有用。
任务状态
任务在其生命周期中可能处于以下几种状态:
- 等待执行 (Pending): 任务已创建但尚未开始执行。
- 正在执行 (Running): 任务正在被线程池中的一个工作线程执行。
- 已完成 (Completed): 任务已成功执行完毕。
- 已取消 (Canceled): 任务在执行完成前被取消。
TaskSystem 基本用法
TaskSystem 提供了简洁的 API 来创建和管理异步任务。以下是一些核心用法示例:
1. 创建并启动简单任务
最简单的任务创建方式,使用 lambda 表达式定义任务体:
// 启动一个默认优先级的简单任务
auto Task = UE::Tasks::Launch(
UE_SOURCE_LOCATION, // 用于调试的位置信息
[]{
UE_LOG(LogTemp, Log, TEXT("Hello from Task!"));
}
);
// 等待任务完成(阻塞当前线程直到任务执行结束)
Task.Wait();
你也可以不持有 FTask
句柄来启动并“忘记”任务,但这会失去对任务状态的控制。在多线程环境下,任务依然会被调度执行。
2. 创建带返回值的任务
可以使用 TTask<T>
来处理带有返回值的任务。任务体需要返回指定类型的值。
// 创建一个返回整数的任务
auto Task = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[]() -> int {
// 执行一些计算
return 42;
}
);
// 获取任务结果(会等待任务完成)
int result = Task.GetResult();
GetResult()
方法会阻塞当前线程直到任务完成并返回结果。
3. 设置任务优先级
可以在启动任务时指定其优先级。
auto Task = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[]{ /* 任务代码 */ },
ETaskPriority::High, // 设置基本优先级为高
EExtendedTaskPriority::None // 设置扩展优先级为无特殊
);
// 使用 FTaskPriorityCVar 从 CVar 读取优先级
UE::Tasks::FTaskPriorityCVar PriorityCVar{ TEXT("MyTaskPriority"), TEXT("任务优先级控制"), ETaskPriority::Normal, EExtendedTaskPriority::None };
auto CVarTask = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[]{ /* 任务代码 */ },
PriorityCVar.GetTaskPriority(),
PriorityCVar.GetExtendedTaskPriority()
);
4. 创建有依赖关系的任務
可以使用 UE::Tasks::Prerequisites
来指定任务的前置条件。依赖任务完成后,后续任务才会被调度执行。
// 创建第一个任务
auto Task1 = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[]{ /* 任务 1 代码 */ }
);
// 创建依赖于 Task1 的任务
auto Task2 = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[]{ /* 任务 2 代码 */ },
UE::Tasks::Prerequisites(Task1) // Task2 会等待 Task1 完成后才开始执行
);
// 可以指定多个前置任务
auto Task3 = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[]{ /* 任务 3 代码 */ },
UE::Tasks::Prerequisites(Task1, Task2)
);
// 可以使用容器来管理前置条件
TArray<UE::Tasks::FTask> PrerequisiteTasks = { Task1, Task2 };
auto Task4 = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[]{ /* 任务 4 代码 */ },
Prerequisites(PrerequisiteTasks)
);
// 可以使用 FTaskEvent 作为前置条件
UE::Tasks::FTaskEvent Event{ UE_SOURCE_LOCATION };
auto Task5 = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[]{ /* 任务 5 代码 */ },
Prerequisites(Event)
);
Event.Trigger(); // 触发事件以允许 Task5 执行
5. 使用不同的可调用对象
虽然 lambda 函数是最常见的选择,但 TaskSystem 也支持其他可调用对象,如函数指针和仿函数。
void Func() { UE_LOG(LogTemp, Log, TEXT("Function executed!")); }
UE::Tasks::Launch(UE_SOURCE_LOCATION, &Func);
struct FFunctor
{
void operator()() { UE_LOG(LogTemp, Log, TEXT("Functor executed!")); }
};
UE::Tasks::Launch(UE_SOURCE_LOCATION, FFunctor{});
6. 管理 FTask
的生命周期
FTask
类似于智能指针,使用引用计数来管理其生命周期。启动任务会启动其生命周期并分配所需的资源。要释放持有的引用,可以“重置”任务句柄。这对于在任务完成后清理资源非常重要,尤其是在 FTask
实例作为成员变量持有的时候。
UE::Tasks::FTask Task = UE::Tasks::Launch(UE_SOURCE_LOCATION, []{});
Task.Wait(); // 确保任务完成
Task = {}; // 释放引用,允许系统回收资源
当 FTask
实例被销毁或赋值时,其内部的引用计数会递减。当引用计数降为零时,相关的资源会被释放。
7. 嵌套任务
一个任务在其执行过程中可以启动新的任务。可以使用 UE::Tasks::AddNested
将内部任务标记为当前任务的子任务。父任务会等待所有通过 AddNested
添加的子任务完成后才算完成。
UE::Tasks::Launch(UE_SOURCE_LOCATION,
[]
{
UE::Tasks::FTask NestedTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [] {});
UE::Tasks::AddNested(NestedTask);
NestedTask.Wait();
}
).Wait();
父任务可以通过 IsAwaitable()
检查自身是否可以安全地被等待,通常在父任务内部等待其嵌套任务完成是有风险的,可能导致死锁。
FPipe 高级用法
FPipe
是 TaskSystem 中的一个重要组件,用于确保任务按顺序执行,并提供了一种管理相互依赖任务的机制。同一个 FPipe
中启动的任务会按照先进先出 (FIFO) 的顺序执行,但与其他 FPipe
或使用 Launch
直接启动的任务并行执行。
1. 基本的 Pipe 使用
// 创建一个命名的 pipe
UE::Tasks::FPipe Pipe{ UE_SOURCE_LOCATION };
// 在 pipe 中启动两个任务,它们会按顺序执行
UE::Tasks::FTask Task1 = Pipe.Launch(UE_SOURCE_LOCATION, [] {});
UE::Tasks::FTask Task2 = Pipe.Launch(UE_SOURCE_LOCATION, [] {});
Task2.Wait(); // 等待 Task2 完成,这也意味着 Task1 已经完成
2. 验证执行顺序
UE::Tasks::FPipe Pipe{ UE_SOURCE_LOCATION };
bool bTask1Done = false;
// 第一个任务会延迟完成
UE::Tasks::FTask Task1 = Pipe.Launch(UE_SOURCE_LOCATION,
[&bTask1Done]
{
FPlatformProcess::Sleep(0.1f);
bTask1Done = true;
}
);
// 第二个任务会验证第一个任务已完成
Pipe.Launch(UE_SOURCE_LOCATION, [&bTask1Done] {
check(bTask1Done);
}).Wait();
3. Pipe 阻塞示例
可以通过 FTaskEvent
来阻塞 Pipe 中的任务执行。
UE::Tasks::FPipe Pipe{ UE_SOURCE_LOCATION };
std::atomic<bool> bBlocked;
UE::Tasks::FTaskEvent Event{ UE_SOURCE_LOCATION };
// 启动一个会阻塞的任务
UE::Tasks::FTask Task = Pipe.Launch(UE_SOURCE_LOCATION,
[&bBlocked, &Event]
{
bBlocked = true;
Event.Wait(); // 任务在这里阻塞
}
);
// 等待任务进入阻塞状态
while (!bBlocked)
{
}
// 此时任务已阻塞
ensure(!Task.Wait(FTimespan::FromMilliseconds(100)));
Event.Trigger(); // 解除阻塞
Task.Wait();
4. 内联任务的 FIFO 顺序
即使是内联执行的任务,在同一个 FPipe
中也会保持 FIFO 的执行顺序。
UE::Tasks::FPipe Pipe{ UE_SOURCE_LOCATION };
UE::Tasks::FTaskEvent Block{ UE_SOURCE_LOCATION };
bool bFirstDone = false;
bool bSecondDone = false;
// 启动两个内联任务,验证它们的执行顺序
UE::Tasks::FTask Task1 = Pipe.Launch(UE_SOURCE_LOCATION,
[&] {
check(!bSecondDone);
bFirstDone = true;
},
Prerequisites(Block),
ETaskPriority::Normal,
EExtendedTaskPriority::Inline
);
UE::Tasks::FTask Task2 = Pipe.Launch(UE_SOURCE_LOCATION,
[&] {
check(bFirstDone);
bSecondDone = true;
},
Prerequisites(Block),
ETaskPriority::Normal,
EExtendedTaskPriority::Inline
);
Block.Trigger();
UE::Tasks::Wait(TArray{ Task1, Task2 });
5. 线程安全的异步接口 (Actor 模式)
FPipe
可以用来构建线程安全的异步接口,类似于简化版的 Actor 模型。所有通过同一个 FPipe
启动的任务都会串行执行,从而避免竞态条件。
class FAsyncClass
{
public:
// 返回带结果的异步操作
UE::Tasks::TTask<bool> DoSomething()
{
return Pipe.Launch(TEXT("DoSomething()"), [this] { return DoSomethingImpl(); });
}
// 返回无结果的异步操作
UE::Tasks::FTask DoSomethingElse()
{
return Pipe.Launch(TEXT("DoSomethingElse()"), [this] { DoSomethingElseImpl(); });
}
private:
bool DoSomethingImpl() { return false; }
void DoSomethingElseImpl() {}
UE::Tasks::FPipe Pipe{ UE_SOURCE_LOCATION }; // 使用 Pipe 确保线程安全
};
// 在多线程中安全地调用 FAsyncClass 的方法
FAsyncClass AsyncInstance;
bool bRes = AsyncInstance.DoSomething().GetResult();
AsyncInstance.DoSomethingElse().Wait();
6. 等待 Pipe 变为空
可以使用 WaitUntilEmpty()
等待 Pipe 中的所有任务完成。
UE::Tasks::FPipe Pipe{ UE_SOURCE_LOCATION };
Pipe.Launch(UE_SOURCE_LOCATION, [] {});
Pipe.Launch(UE_SOURCE_LOCATION, [] {});
Pipe.WaitUntilEmpty();
TaskSystem 高级功能
除了基本的启动和依赖管理外,TaskSystem 还提供了一些高级功能来处理更复杂的异步场景。
1. 任务事件 (FTaskEvent)
FTaskEvent
用于同步多个任务的执行。一个任务可以等待一个事件被触发后继续执行。
// 创建任务事件
UE::Tasks::FTaskEvent Event(TEXT("MyEvent"));
// 添加事件的前置条件(可选)
// Event.AddPrerequisites(SomePrerequisites);
// 在某个时刻触发事件
Event.Trigger();
// 使用事件作为任务的前置条件
auto Task = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[]{ /* 任务代码 */ },
UE::Tasks::Prerequisites(Event)
);
// 任务可以等待事件被触发
UE::Tasks::FTask WaitingTask = UE::Tasks::Launch(UE_SOURCE_LOCATION,
[&Event] {
Event.Wait(); // 任务会在这里阻塞直到事件被触发
UE_LOG(LogTemp, Log, TEXT("Event triggered, task continuing."));
}
);
2. 任务取消 (CancellationToken)
可以使用 FCancellationToken
来取消正在执行或等待执行的任务。
// 创建取消令牌
UE::Tasks::FCancellationToken Token;
// 创建可取消的任务
auto Task = UE::Tasks::Launch(
UE_SOURCE_LOCATION,
[&Token]{
// 定期检查是否被取消
for (int i = 0; i < 10; ++i) {
if (Token.IsCanceled()) {
UE_LOG(LogTemp, Warning, TEXT("Task cancelled."));
return;
}
FPlatformProcess::Sleep(0.1f);
UE_LOG(LogTemp, Log, TEXT("Task running..."));
}
}
);
// 在需要时取消任务
Token.Cancel();
// 你可以创建一个已完成的任务
auto CompletedTask = UE::Tasks::MakeCompletedTask<int>(42);
check(CompletedTask.GetResult() == 42);
3. 等待多个任务
TaskSystem 提供了等待一个或多个任务完成的便捷方法。
// 创建任务数组
TArray<UE::Tasks::FTask> Tasks;
// 添加多个任务
Tasks.Add(UE::Tasks::Launch(UE_SOURCE_LOCATION, []{}));
Tasks.Add(UE::Tasks::Launch(UE_SOURCE_LOCATION, []{}));
// 等待所有任务完成
UE::Tasks::Wait(Tasks);
// 等待任意一个任务完成,返回已完成任务的索引
int32 CompletedTaskIndex = UE::Tasks::WaitAny(Tasks);
// 创建一个等待任意任务完成的 Task
UE::Tasks::FTask AnyTask = UE::Tasks::Any(Tasks);
AnyTask.Wait();
4. 任务并发限制器 (FTaskConcurrencyLimiter)
FTaskConcurrencyLimiter
允许限制特定类型任务的并发执行数量,这对于控制资源消耗非常有用。
UE::Tasks::FTaskConcurrencyLimiter Limiter(4); // 允许最多 4 个任务并发执行
for (int i = 0; i < 10; ++i)
{
Limiter.Push(UE_SOURCE_LOCATION, [](uint32 Slot) {
UE_LOG(LogTemp, Log, TEXT("Task executing in slot %d"), Slot);
FPlatformProcess::Sleep(0.1f);
});
}
Limiter.Wait(); // 等待所有通过 Limiter 派发的任务完成
Push
方法接受一个 lambda,该 lambda 接收一个表示当前 slot 的 uint32
参数,可以用于区分不同的并发执行单元。
总结
UE5 的 TaskSystem 提供了一套强大且灵活的工具,用于管理游戏中的异步操作。无论是简单的后台任务,还是复杂的依赖关系和线程同步,TaskSystem 都能提供合适的 API 来实现。理解其核心概念和高级功能,能帮助开发者更好地利用多核处理器的性能,并创建响应迅速、高效的游戏体验。