UE5 线程进阶系列又见面了。上一篇我们盯着 FRunnable、FRunnableThread 这种最底层的线程玩法讲了半天,相信不少同学都有一个直观感受:手写一个线程类,又要重写 Run(),又要管生命周期,还要清理内存,实在太重了。这一篇我们把目光转到引擎给你封装好的三个全局异步函数——AsyncPool、AsyncThread、AsyncTask,它们都定义在 Async/Async.h 里,头文件一加,随手就能把一段逻辑丢到后台去跑,还能把结果拿回来。
简单概括一下定位:AsyncPool 把作业交给全局线程池,线程池里的线程大家共用;AsyncThread 更直接,为当前这个作业单开一条线程,跑完就销毁;AsyncTask 则是走任务图调度,既能投递给任意后台线程,也能精确指定让任务跑在游戏线程、渲染线程上。这三个函数覆盖了日常开发里“想开个线程干点活”的绝大多数场景。这篇主要适合两类人:一是刚把 FRunnable 磕磕绊绊跑通、想找更省心写法的开发者;二是已经会用 FAsyncTask 但一直没搞懂这几个全局函数区别的人。看完你最少能对着需求一眼挑对函数,并且避开线程池被阻塞、UObject 跨线程访问这类经典大坑。
1. 先理清三个执行模型:线程池、独立线程、任务图
1.1 三种“后台跑代码”的方式,到底差在哪
先说线程池。你可以把它想象成一支固定人数的工位团队,任务来了谁有空谁接。好处是线程被反复复用,创建线程的开销只发生一次,后续任务进来几乎是零成本切换。坏处是团队人数固定,如果有人手头的活一直干不完,后面排队的任务就得等。AsyncPool 走的就是这条路。
再说独立线程。相当于为了这件事专门雇一个人,干完活就走,下次要再干再雇。好处是执行环境完全独立,哪怕任务里阻塞放死等,也不影响别人。坏处是“雇人”这件事本身有成本,注册线程对象、分配栈、系统调度,一套流程下来怎么也得消耗一点时间。AsyncThread 是这条路。
最后是任务图。任务图可以理解成一个中央厨房调度系统:你提交任务的时候不光提交“做什么”,还可以声明“依赖什么”。调度器把任务分配给空闲的 worker,并且支持工作窃取,哪里有空往哪里填。它最擅长的是大量细粒度小任务,以及需要精确跨线程跳转的场景。引擎的渲染、动画、物理很多并行系统底层都在用同一套任务图基础设施。AsyncTask 就是给普通业务代码开放的入口。
这里有一个核心观念:后台执行不是免费的。线程池线程数量有限、线程创建有开销、任务图调度再轻也要走一次任务对象。所以用哪个 API,本质上是在“复用成本”和“隔离性”之间做取舍。
1.2 三个函数的调用形式与返回类型
先给一个概念性的签名,实际源码里都是函数模板,返回类型由你传入的可调用对象自动推导:
cpp复制#include "Async/Async.h"
// 在线程池里执行,InThread 一般传 Any 系列
template <typename TFunction>
auto AsyncPool(ENamedThreads::Type InThread, TFunction&& InFunction, const ANSICHAR* InTraceName = nullptr);
// 单独开一条线程执行,可指定线程优先级
template <typename TFunction>
auto AsyncThread(TFunction&& InFunction, const ANSICHAR* InTraceName = nullptr, EThreadPriority InThreadPriority = TPri_Normal);
// 走任务图调度,InThread 可以指定 GameThread / RenderThread / AnyThread 等
template <typename TFunction>
auto AsyncTask(ENamedThreads::Type InThread, TFunction&& InFunction, const ANSICHAR* InTraceName = nullptr);
三个函数的返回值都是 TFuture<TReturn>,TReturn 由你传入的 lambda 的返回类型决定。比如传入 []() -> int32 { return 42; },拿到的就是 TFuture<int32>。TFuture 是引擎对 future/promise 模式的实现,有几个方法你平时一定会碰到:
IsReady():任务完成了吗?非阻塞,轮询友好。Wait():阻塞当前线程直到任务完成。Get():阻塞等待并取出结果。对TFuture<void>来说,Get()的作用就是等一下,没有返回值。Then(...):任务完成之后自动接一段延续逻辑,不用手动轮询。
这里必须强调一句:Get() 和 Wait() 会阻塞调用线程。如果你在游戏线程里无脑 Wait(),游戏线程就被卡住了,这和“异步”两个字是背道而驰的。新手最容易犯的错误就是把 future 当成线程句柄来等,等来等去把主线程等卡了。
1.3 统一入口 Async(EAsyncExecution, ...) 是什么
如果你不想记三个函数,引擎还提供了一个统一的入口 Async。它的第一个参数是一个枚举 EAsyncExecution,源码内部其实就是转发到上面三个函数:
cpp复制enum class EAsyncExecution
{
ThreadPool, // 映射到 AsyncPool(ENamedThreads::AnyHiPriThreadHiPriTask, ...)
Thread, // 映射到 AsyncThread(..., TPri_Normal)
TaskGraph, // 映射到 AsyncTask(ENamedThreads::AnyHiPriThreadHiPriTask, ...)
TaskGraphMainThread // 映射到 AsyncTask(ENamedThreads::GameThread, ...)
};
所以你可以写 Async(EAsyncExecution::ThreadPool, [] { ... }),也可以直接写 AsyncPool(...)。我更
