Awaitable 类是自定义 Unity 类型,可在 C# 异步编程模型中等待并用作异步返回类型。Unity 的大多数异步 API 都支持 async 和 await 模式,包括:
NextFrameAsync、WaitForSecondsAsync、EndOfFrameAsync、FixedUpdateAsync
AsyncOperation 的所有类型您可以在自己的代码中将 Awaitable 类与 await 运算符结合使用,并作为 async 返回类型使用,如下所示:
async Awaitable<List<Achievement>> GetAchievementsAsync()
{
var apiResult = await SomeMethodReturningATask(); // or any await-compatible type
List<Achievement> achievements = JsonConvert.DeserializeObject<List<Achievement>>(apiResult);
return achievements;
}
async Awaitable ShowAchievementsView()
{
ShowLoadingOverlay();
List<Achievement> achievements = await GetAchievementsAsync();
HideLoadingOverlay();
ShowAchivementsList(achievements);
}
Awaitable 旨在为 Unity 项目中的异步代码提供更有效的 .NET Task 替代方案。与 Task 相比,Awaitable 的效率存在一些重要限制。
最重要的限制是 Awaitable 实例会被池化以限制分配。请参考以下示例:
class SomeMonoBehaviorWithAwaitable : MonoBehavior
{
public async void Start()
{
while(true)
{
// do some work on each frame
await Awaitable.NextFrameAsync();
}
}
}
如果不池化,此示例中的每个 MonoBehavior 实例都会每帧分配一个 Awaitable 对象,从而增加垃圾回收器工作量并降低性能。为了缓解此问题,Unity 会在等待 Awaitable 对象后将其返回到内部 Awaitable 池。
重要提示:Awaitable 实例池意味着在一个 Awaitable 实例上多次 await 永远不会安全。这样做可能会导致未定义的行为,例如异常或死锁。
.NET ValueTask<TResult> 具有与 Awaitable 相同的一些主要优点和限制。ValueTask 的典型推荐用途是用于大多数时间预期同步完成的异步工作负载。有关更多信息,请参阅了解使用 ValueTask 的原因、方式和时间。
下表总结了 Unity 的 Awaitable 类与 .NET Task 和 ValueTask 之间的功能比较:
| 功能 | Task |
ValueTask |
UnityEngine.Awaitable |
|---|---|---|---|
| 所需分配 |
多次。 在每次调用 Task 返回方法时进行分配,因此会增加内存使用量和垃圾回收器工作量。 |
根据需要。 可利用池化功能进行优化。 |
按需最低程度。 调用 Awaitable 返回方法通常不会分配内存,因为默认情况下会池化 Awaitable 实例。 |
| 可安全地等待多次 | 是。 |
否。 必须转换为具有 ValueTask.AsTask 的 Task。 |
否。 必须转换为具有自定义 AsTask 扩展方法的 Task,请参阅代码示例参考中的“在同一方法中等待多次”。 |
| 延续异步运行 |
是。 默认情况下使用同步上下文,否则使用 ThreadPool。这会增加 Unity 中主线程完成时的延迟,因为代码必须等到下一帧 Update 才能恢复。 |
是。 针对等待的任务同步完成的情况进行了优化。如果是异步完成的,则继续行为等同于 Task。 |
否。 触发完成时,继续同步运行,这意味着代码会立即在触发完成的同一帧中恢复。请参阅“Awaitable 完成和延续”以了解更多信息。 |
| 可通过代码触发完成 |
是。 使用 TaskCompletionSource。 |
不适用于典型的用例,这种情况下的任务大多同步完成。 |
是。 使用 AwaitableCompletionSource。 |
| 可以返回值 |
是。 使用 Task<TResult>。 |
是。 使用 ValueTask<TResult>。 |
是。 使用 UnityEngine.Awaitable<T>。 |
内置对 WaitAll 和 WaitAny 的支持 |
是。 |
否。 必须转换为具有 ValueTask.AsTask 的 Task。 |
否。 必须转换为具有自定义 AsTask 扩展方法的 Task,请参阅代码示例参考中的“在 .NET Task 中封装 Awaitable”。 |
| Unity 线程和更新循环感知执行调度 | 否。 | 否。 |
是。 您可以使用 Awaitable.BackgroundThreadAsync 和 Awaitable.MainThreadAsync 指定在哪个线程上恢复 Awaitable。还可以使用 Awaitable.NextFrameAsync 和 Awaitable.FixedUpdateAsync 来调度相对于 Update 或 FixedUpdate 循环的工作。有关更多信息,请参阅“Awaitable 完成和延续”。 |
API 的选择取决于异步代码的性能配置文件,但一般情况下:
Task 是唯一的选择。ValueTask 是不错的选择。Awaitable 在以下情况下是不错的选择:
Awaitable 协程通常比基于迭代器的协程更加高效,尤其是在迭代器返回非 null 值的情况下,例如 WaitForFixedUpdate。
但是,当同时运行许多 Awaitable 协程时,协程的性能优势会降低。例如,在 while 循环中等待 Awaitable.NextFrameAsync 的 MonoBehaviour(如前一个代码示例所示)如果附加到大型项目中的每个游戏对象,可能会导致性能问题。