版本:Unity 6 (6000.0)
语言:English
代码优化
作业系统

异步编程

Unity 使用 .NET 的 async 关键字和 await 运算符支持简化的异步编程模型。有关 .NET 中异步编程的介绍,请参阅 Microsoft 文档中的 使用 async 和 await 进行异步编程

大多数 Unity 的异步 API 都支持asyncawait 模式,包括

您还可以将 Awaitable 类与 await 运算符以及作为您自己代码中的 async 返回类型一起使用,如下所示

async Awaitable<List<Achievement>> GetAchievementsAsync()
{
    var apiResult = await SomeMethodReturningATask(); // or any await-compatible type
    JsonConvert.DeserializeObject<List<Achievement>>(apiResult);
    return apiResult;
}

async Awaitable ShowAchievementsView()
{
    ShowLoadingOverlay();
    var achievements = await GetAchievementsAsync();
    HideLoadingOverlay();
    ShowAchivementsList(achievements);
}

在大多数情况下,在创建异步方法时,您应该优先使用 Awaitable 而不是 .NET Task。

System.Threading.Tasks.Task 与 UnityEngine.Awaitable

Unity 的 Awaitable 类旨在在 Unity 项目中尽可能高效,但是与 .NET 任务 相比,这种效率带来了一些权衡。最显著的限制是 Awaitable 实例被池化以限制分配。例如,请考虑以下示例

class SomeMonoBehaviorWithAwaitable : MonoBehavior
{
    public async void Start()
    {
        while(true)
        {
            // do some work on each frame
            await Awaitable.NextFrameAsync();
        }
    }
}

如果没有池化,此行为的每个实例都会在每一帧分配一个 Awaitable 对象。这会给垃圾回收器带来很大的压力,导致性能下降。为了缓解这种情况,一旦等待完成,Unity 会将 Awaitable 对象返回到内部 Awaitable 池。

注意:这有一个重要的含义:您绝不应Awaitable 实例进行多次 await 操作。这样做会导致未定义的行为,例如异常或死锁。

下表列出了 Unity 的 Awaitable 类与 .NET 任务之间其他值得注意的差异和相似之处。

  System.Threading.Tasks.Task UnityEngine.Awaitable
可以多次等待
可以返回值 是,使用System.Threading.Tasks.Task<T> 是,使用UnityEngine.Awaitable<T>
可以通过代码触发完成 是,使用System.Threading.Tasks.TaskCompletionSource 是,使用UnityEngine.AwaitableCompletionSource
延续异步运行 是,默认情况下使用同步上下文,否则使用线程池 否,当触发完成时,延续同步运行

任务延续和同步上下文

System.Threading.Tasks.Task 延续在调用 API 时处于活动状态的同步上下文中运行,或者如果未设置同步上下文,则通过 线程池 运行。等待 Taskasync 方法将在环境同步上下文中恢复。在 Unity 上下文中,延续将发布到 UnitySynchronizationContext 并在主线程上的下一个 Update 滴答中执行。请考虑以下示例

void PrintA()
{
    DoSomething();

    for (int i = 0; i < 100000; ++i)
    {
        // do something else
        print("A");
    }
}

async void DoSomething()
{
    await Task.Delay(1);
    print("Do something"):
}

如果您在 Unity 中运行此代码,“执行某些操作”将直到最后一个“A”之后才会出现,因为 PrintA 不是 async 方法,并且会阻塞主线程直到它完成。

Awaitable 延续和同步上下文

Awaitable 延续在操作完成后同步运行。大多数 Unity API 不是线程安全的,因此,您应该只从主线程使用 Unity API。Unity 使用自定义 UnitySynchronizationContext 覆盖默认的 SynchronizationContext,并默认情况下在编辑播放模式下在主线程上运行所有 .NET 任务延续。

除非另有说明,否则 Unity API 返回的所有 Awaitable 都会在主线程上完成,因此无需捕获同步上下文。但是,您可以编写代码以执行其他操作。请参阅以下示例

private async Awaitable<float> DoSomeHeavyComputationInBackgroundAsync()
{
    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 DoSomeHeavyComputationInBackgroundAsync();
    await SceneManager.LoadSceneAsync("my-scene"); // this will fail as SceneManager.LoadAsync only works from main thread
}

为了改进这种情况,您应该确保您的 Awaitable 返回方法默认在主线程上完成。这是一个示例,其中 DoSomeHeavyComputationInBackgroundAsync 默认在主线程上完成,同时允许调用者在后台线程上显式继续(以便在后台线程上链接繁重的计算操作,而无需同步回主线程)

private async Awaitable<float> DoSomeHeavyComputationInBackgroundAsync(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 DoSomeHeavyComputationInBackgroundAsync();
    await SceneManager.LoadSceneAsync("my-scene"); // this is ok!
}

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

开发版本开发版本包含调试符号并启用探查器。 更多信息
请参阅 术语表
中,如果您尝试在多线程代码中使用 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 不会阻止在正式版本中执行多线程代码,但如果您确实使用了多个线程,则可能会出现随机崩溃和其他不可预测的错误。因此,在编写可能在后台线程中运行的代码时,您需要格外小心调用哪些 API。出于这个原因,您不应该使用自己的多线程,而应该使用 Unity 的 作业系统。作业系统安全地使用多个线程并行执行作业,并实现多线程的性能优势。有关更多信息,请参阅 作业系统概述

使用 Awaitable.BackgroundThreadAsync 并使用 Awaitable.MainThreadAsync 返回主线程适用于相对较长时间运行的后台操作(例如,超过一帧),以避免阻塞主游戏循环,但不适用于利用单帧内多核 CPU。

AwaitableCompletionSource

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 应该比基于迭代器的协程略微高效,尤其是在迭代器将返回非空值(例如 WaitForFixedUpdate 等)的情况下。

可扩展性

尽管 Unity 的 Awaitable 类针对性能进行了优化,但您应该避免运行数十万个并发协程。与基于协程的迭代器类似,将类似于以下示例的循环行为附加到所有游戏对象很可能会导致性能问题

while(true){
    // do something
    await Awaitable.NextFrameAsync();
}

MainThreadAsync / BackgroundThreadAsync

从性能角度来看,从主线程调用 await Awaitable.MainThreadAsync() 或从后台线程调用 await Awaitable.BackgroundThreadAsync() 非常高效。但是,从后台线程切换到主线程的同步机制会导致您的代码在下一个 Update 事件中恢复。因此,您应该避免频繁地在主线程和后台线程之间来回切换,因为您的代码在每次调用 MainThreadAsync() 时都必须等待下一帧。

从主线程调用返回 Task 的方法

如果您从 Unity 主线程调用返回 Task 的方法,则延续将在主帧中执行。如果您从后台线程调用它,它将在线程池线程上完成。从主线程调用返回 Task 的 API 会增加延迟。如果它没有同步完成,您将需要至少等待下一个 Update 滴答(在 30fps 下为 33ms)才能运行延续。

如果网络延迟是一个问题,建议在主线程之外执行此操作,并使用自定义逻辑在主线程和 网络Unity 系统,通过计算机网络实现多人游戏。 更多信息
请参阅 术语表
任务之间进行同步。

与 C# 作业系统相比的 Await 支持

与 C# 作业系统相比,Unity 的 Await 支持更适合以下场景

  • 在处理本质上异步的操作(例如以非阻塞方式操作文件或执行 Web 请求)时简化代码。
  • 将长时间运行的任务(>1 帧)卸载到后台线程。
  • 对基于迭代器的协程进行现代化。
  • 混合和匹配多种异步操作(帧事件、Unity 事件、第三方异步 API、I/O)。

但是,它建议用于较短寿命的操作,例如并行化计算密集型算法时。要充分利用多核 CPU 并并行化您的算法,您应该改用 C# 作业系统

测试

Unity 的测试框架不识别 Awaitable 作为有效的测试返回类型。但是,以下示例显示了如何使用 Awaitable 的 IEnumerator 实现来编写异步测试

[UnityTest]
public IEnumerator SomeAsyncTest(){
    async Awaitable TestImplementation(){
        // test something with async / await support here
    };
    return TestImplementation();
}

示例

帧协程

async Awaitable SampleSchedulingJobsForNextFrame()
{
    // wait until end of frame to avoid competing over resources with other unity subsystems
    await Awaitable.EndOfFrameAsync(); 
    var jobHandle = ScheduleSomethingWithJobSystem();
    // let the job execute while next frame starts
    await Awaitable.NextFrameAsync();
    jobHandle.Complete();
    // use results of computation
}

JobHandle ScheduleSomethingWithJobSystem()
{
    ...
}

切换到后台线程并返回

private async Awaitable<float> DoSomeHeavyComputationInBackgroundAsync(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 operation = Resources.LoadAsync("my-texture");
    await operation;
    var texture = operation.asset as Texture2D;
}

组合

利用 await 的最大优势之一是,我们可以在同一个方法中混合和匹配任何兼容 await 的类型

public async Awaitable Start()
{
    await CallSomeThirdPartyAPIReturningDotnetTask();
    await Awaitable.NextFrameAsync();
    await SceneManager.LoadSceneAsync("my-scene");
    await SomeUserCodeReturningAwaitable();
    ...
}

其他资源

代码优化
作业系统