文档

Unity Engine

Unity Engine

使用 Awaitable 进行异步编程的简介

Understand the key features of Unity's Awaitable and how it compares to both .NET Task and iterator-based coroutines.
阅读时间7 分钟最后更新于 1 个月前

Awaitable
类是自定义 Unity 类型,可在 C# 异步编程模型中等待并用作异步返回类型。Unity 的大多数异步 API 都支持
async
和
await
模式,包括:
您可以在自己的代码中将
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 与 .NET Task 相比

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
永远不会安全。这样做可能会导致未定义的行为,例如异常或死锁。

Awaitable 与 .NET ValueTask 相比

.NET
ValueTask<TResult>
具有与
Awaitable
相同的一些主要优点和限制。
ValueTask
的典型推荐用途是用于大多数时间预期同步完成的异步工作负载。有关更多信息,请参阅了解使用 ValueTask 的原因、方式和时间。

Awaitable、Task 和 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 完成和延续”。

何时使用 Awaitable 而非 Task 或 ValueTask

API 的选择取决于异步代码的性能配置文件,但一般情况下:
  • 当您需要多次等待或同时等待多个消耗者时,
    Task
    是唯一的选择。
  • 如果您的高吞吐量异步代码大多数时间都是同步完成的,
    ValueTask
    是不错的选择。
  • Awaitable
    在以下情况下是不错的选择:
    • 您无需多次等待您的方法并期望基本上异步完成。
    • 您希望异步任务具有对特定于 Unity 的概念(如主线程以及 Update 和 FixedUpdate 循环)的内置支持。

Awaitable 相比于基于迭代器的协程

Awaitable
协程通常比基于迭代器的协程更加高效,尤其是在迭代器返回非 null 值的情况下,例如
WaitForFixedUpdate
。
但是,当同时运行许多
Awaitable
协程时,协程的性能优势会降低。例如,在
while
循环中等待
Awaitable.NextFrameAsync
的 MonoBehaviour(如前一个代码示例所示)如果附加到大型项目中的每个游戏对象,可能会导致性能问题。

其他资源