Version: Unity 6.0 (6000.0)
语言 : 中文
使用 Awaitable 进行异步编程的简介
Awaitable 代码示例参考

Awaitable 完成和延续

await 运算符会暂停执行附加的异步方法,允许调用线程在等待时执行其他工作。当等待的 TaskAwaitable 完成时,异步代码需要从被暂停的点开始恢复并继续执行。异步代码的恢复方式可能会对应用程序的功能和性能产生重大影响。

.NET Task 延续

开始等待时所处的状态代码相关信息称为同步上下文。.NET 平台提供了用于捕获此类信息的 SynchronizationContext 类。Task 延续将在调用异步方法的同步上下文中运行,如果未设置同步上下文,则通过线程池运行。

大多数 Unity API 都不是线程安全的,只能从主线程调用。因此,Unity 会使用自定义 UnitySynchronizationContext 覆盖默认 SynchronizationContext,以确保所有 .NET Task 延续在编辑模式和运行模式下都默认在主线程上运行。如果从 Unity 主线程调用 Task 返回方法,则延续将发布到 UnitySynchronizationContext,并在主线程上的下一帧 Update 刷新时运行。如果从后台线程调用,则会在线程池线程上完成。

捕获同步上下文会增加应用程序的性能开销,而在主线程上等待下一帧恢复更新会引入大规模延迟。您可以改用 Awaitable 来避免这两个问题。

Awaitable 延续

除非另有说明,否则 Unity API 返回的所有 Awaitable 实例都在主线程上完成,因此无需捕获同步上下文。Awaitable 延续将在操作完成后同步运行,这意味着代码可以立即恢复,而不必等待下一帧。

您可以使用 Awaitable.BackgroundThreadAsync 显式重载默认延续行为。以下示例指定了 Awaitable 在后台线程上完成:

private async Awaitable<float> DoHeavyComputationInBackgroundAsync()
{
    await Awaitable.BackgroundThreadAsync();
    // do some heavy math here
    return 42; // this will make the returned awaitable complete on a background thread
}

public async Awaitable Start()
{
    var computationResult = await DoHeavyComputationInBackgroundAsync();
    await SceneManager.LoadSceneAsync("my-scene"); // this will fail as SceneManager.LoadAsync only works from main thread
}

在这种情况下,将 Awaitable 设置为在后台线程上完成会产生问题。在后台线程上恢复 Start 方法时,对 SceneManager.LoadSceneAsync 的下一次调用会失败,因为 SceneManager.LoadSceneAsync 仅在主线程上有效。

要解决这种情况,可以使用 Awaitable.MainThreadAsync 使 Awaitable 返回方法默认在主线程上完成,同时仍然提供后台线程工作的选项。在以下示例中,默认情况下,DoHeavyComputationInBackgroundAsync 在主线程上完成之前会在后台线程上做一些工作。continueOnMainThread 参数允许调用者在适当时在后台线程上完成,例如在链接繁重的计算操作时:

private async Awaitable<float> DoHeavyComputationInBackgroundAsync(bool continueOnMainThread = true)
{
    await Awaitable.BackgroundThreadAsync();
    // do some heavy math here
    float result = 42;

    // by default, switch back to main thread:
    if(continueOnMainThread){
        await Awaitable.MainThreadAsync();
    }
    return result;
}

public async Awaitable Start()
{
    var computationResult = await DoHeavyComputationInBackgroundAsync();
    await SceneManager.LoadSceneAsync("my-scene"); // this is ok!
}

注意:退出运行模式时,Unity 不会自动停止在后台运行的代码。要在退出运行模式时取消后台操作,请使用 Application.exitCancellationToken

线程切换和性能

从主线程调用 await Awaitable.MainThreadAsync() 和从后台线程调用 await Awaitable.BackgroundThreadAsync() 是最有效的方法,因为在每个情况下,代码会在完成后立即恢复。如果从具有 MainThreadAsync 的后台线程切换回主线程,则在主线程的下一帧更新之前无法恢复代码。

如果从主线程调用 Task 返回 API,但未同步完成,则需要至少等待下一次 Update 刷新(33 毫秒,30fps),延续才能运行。如果担心网络延迟,建议在主线程之外执行此操作,并使用自定义逻辑在主线程和网络任务之间同步。

在开发构建中,如果尝试在多线程代码中使用 Unity API,Unity 会显示以下错误消息:

UnityException: Internal_CreateGameObject can only be called from the main thread.

Constructors and field initializers will be executed from the loading thread when loading a scene.

Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.

重要提示:出于性能原因,Unity 不会检查非开发构建中的多线程行为,也不会在上线构建中显示此错误。虽然 Unity 不会阻止在这些上下文中执行多线程代码,但如果确实使用多线程,则可能会发生崩溃和其他不可预测的错误。使用 Unity 的作业系统比使用自己的多线程更安全。作业系统使用多线程安全地并行执行作业,并且可以实现多线程的性能优势。有关更多信息,请参阅作业系统概述

Awaitable 与作业系统相比

Unity 的 Awaitable 类比作业系统更适合以下情况:

  • 在处理固有异步操作(例如以非阻塞方式操作文件或执行 Web 请求)时简化代码。
  • 将长期运行的任务(>1 帧)转移到后台线程。
  • 更新基于迭代器的协程。
  • 等待多种异步操作(帧事件、Unity 事件、第三方异步 API、I/O)。

但是,不建议用于生命周期较短的操作,例如并行执行计算量大的算法。要充分利用多核 CPU 且并行执行算法,请改用作业系统

从代码触发完成

AwaitableCompletionSourceAwaitableCompletionSource<T> 允许创建从用户代码中引发完成的 Awaitable 实例。例如,这可用于实现用户提示,而无需实现状态机来等待用户交互完成:


public class UserNamePrompt : MonoBehavior 
{
    TextField _userNameTextField;
    AwaitableCompletionSource<string> _completionSource = new AwaitableCompletionSource<string>();
    public void Start()
    {
        var rootVisual = GetComponent<UIDocument>().rootVisualElement;
        var userNameField = rootVisual.Q<TextField>("userNameField");
        rootVisual.Q<Button>("OkButton").clicked += ()=>{
            _completionSource.SetResult(userNameField.text);
        }
    }

    public Awaitable<string> WaitForUsernameAsync() => _completionSource.Awaitable;
}

...

public class HighScoreRanks : MonoBehavior 
{
    ...
    public async Awaitable ReportCurrentUserScoreAsync(int score)
    {
        _userNameOverlayGameObject.SetActive(true);
        var prompt = _userNameOverlayGameObject.GetComponent<UserNamePrompt>();
        var userName = await prompt.WaitForUsernameAsync();
        _userNameOverlayGameObject.SetActive(false);
        await SomeAPICall(userName, score);
    }
}

其他资源

使用 Awaitable 进行异步编程的简介
Awaitable 代码示例参考
Copyright © 2023 Unity Technologies
优美缔软件(上海)有限公司 版权所有
"Unity"、Unity 徽标及其他 Unity 商标是 Unity Technologies 或其附属机构在美国及其他地区的商标或注册商标。其他名称或品牌是其各自所有者的商标。
公安部备案号:
31010902002961