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

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

Awaitable 类是自定义 Unity 类型,可在 C# 异步编程模型中等待并用作异步返回类型。Unity 的大多数异步 API 都支持 asyncawait 模式,包括:

您可以在自己的代码中将 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 TaskValueTask 之间的功能比较:

功能 Task ValueTask UnityEngine.Awaitable
所需分配 多次
在每次调用 Task 返回方法时进行分配,因此会增加内存使用量和垃圾回收器工作量。
根据需要
可利用池化功能进行优化。
按需最低程度
调用 Awaitable 返回方法通常不会分配内存,因为默认情况下会池化 Awaitable 实例。
可安全地等待多次
必须转换为具有 ValueTask.AsTaskTask

必须转换为具有自定义 AsTask 扩展方法的 Task,请参阅代码示例参考中的“在同一方法中等待多次”
延续异步运行
默认情况下使用同步上下文,否则使用 ThreadPool。这会增加 Unity 中主线程完成时的延迟,因为代码必须等到下一帧 Update 才能恢复。

针对等待的任务同步完成的情况进行了优化。如果是异步完成的,则继续行为等同于 Task

触发完成时,继续同步运行,这意味着代码会立即在触发完成的同一帧中恢复。请参阅“Awaitable 完成和延续”以了解更多信息。
可通过代码触发完成
使用 TaskCompletionSource
不适用于典型的用例,这种情况下的任务大多同步完成。
使用 AwaitableCompletionSource
可以返回值
使用 Task<TResult>

使用 ValueTask<TResult>

使用 UnityEngine.Awaitable<T>
内置对 WaitAllWaitAny 的支持
必须转换为具有 ValueTask.AsTaskTask

必须转换为具有自定义 AsTask 扩展方法的 Task,请参阅代码示例参考中的“在 .NET Task 中封装 Awaitable”
Unity 线程和更新循环感知执行调度
您可以使用 Awaitable.BackgroundThreadAsyncAwaitable.MainThreadAsync 指定在哪个线程上恢复 Awaitable。还可以使用 Awaitable.NextFrameAsyncAwaitable.FixedUpdateAsync 来调度相对于 UpdateFixedUpdate 循环的工作。有关更多信息,请参阅“Awaitable 完成和延续”

何时使用 Awaitable 而非 Task 或 ValueTask

API 的选择取决于异步代码的性能配置文件,但一般情况下:

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

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

Awaitable 协程通常比基于迭代器的协程更加高效,尤其是在迭代器返回非 null 值的情况下,例如 WaitForFixedUpdate

但是,当同时运行许多 Awaitable 协程时,协程的性能优势会降低。例如,在 while 循环中等待 Awaitable.NextFrameAsync 的 MonoBehaviour(如前一个代码示例所示)如果附加到大型项目中的每个游戏对象,可能会导致性能问题。

其他资源

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