Version: Unity 6.0 (6000.0)
语言 : 中文
使用 Awaitable 类进行异步编程
Awaitable 完成和延续

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

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(如前一个代码示例所示)如果附加到大型项目中的每个游戏对象,可能会导致性能问题。

其他资源

使用 Awaitable 类进行异步编程
Awaitable 完成和延续