# Awaitable 完成和延续

> Understand how asynchronous code resumes on completion of an awaited task and how this affects the function and performance of your application.

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

## .NET Task 延续##.net-task-continuations

开始等待时所处的状态代码相关信息称为同步上下文。.NET 平台提供了用于捕获此类信息的 [`SynchronizationContext`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext?view=net-8.0) 类。`Task` 延续将在调用异步方法的同步上下文中运行，如果未设置同步上下文，则通过[线程池](https://learn.microsoft.com/en-us/dotnet/api/system.threading.threadpool?view=net-8.0)运行。

大多数 Unity API 都不是线程安全的，只能从主线程调用。因此，Unity 会使用自定义 [`UnitySynchronizationContext`](https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnitySynchronizationContext.cs) 覆盖默认 [`SynchronizationContext`](https://docs.microsoft.com/en-us/archive/msdn-magazine/2011/february/msdn-magazine-parallel-computing-it-s-all-about-the-synchronizationcontext)，以确保所有 .NET `Task` 延续在编辑模式和运行模式下都默认在主线程上运行。如果从 Unity 主线程调用 `Task` 返回方法，则延续将发布到 `UnitySynchronizationContext`，并在主线程上的下一帧 `Update` 刷新时运行。如果从后台线程调用，则会在线程池线程上完成。

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

## Awaitable 延续##awaitable-continuations

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

您可以使用 [`Awaitable.BackgroundThreadAsync`](/engine/6000.0/script-reference/unity-engine/awaitable/background-thread-async.md) 显式重载默认延续行为。以下示例指定了 `Awaitable` 在后台线程上完成：

```lang-csharp
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`](/engine/6000.0/script-reference/unity-engine/awaitable/main-thread-async.md) 使 `Awaitable` 返回方法默认在主线程上完成，同时仍然提供后台线程工作的选项。在以下示例中，默认情况下，`DoHeavyComputationInBackgroundAsync` 在主线程上完成之前会在后台线程上做一些工作。`continueOnMainThread` 参数允许调用者在适当时在后台线程上完成，例如在链接繁重的计算操作时：

```lang-csharp
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`](/engine/6000.0/script-reference/unity-engine/application/exit-cancellation-token.md)。

## 线程切换和性能##thread-switching-and-performance

从主线程调用 `await Awaitable.MainThreadAsync()` 和从后台线程调用 `await Awaitable.BackgroundThreadAsync()` 是最有效的方法，因为在每个情况下，代码会在完成后立即恢复。如果从具有 `MainThreadAsync` 的后台线程切换回主线程，则在主线程的下一[帧更新](/engine/6000.0/manual/scripting/object-oriented-development/managing-time-and-frame-rate/time-per-frame-updates.md)之前无法恢复代码。

如果从主线程调用 `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 不会检查[非开发构建](/engine/6000.0/manual/unity-editor/editor-features/build-settings/build-profiles-reference.md)中的多线程行为，也不会在上线构建中显示此错误。虽然 Unity 不会阻止在这些上下文中执行多线程代码，但如果确实使用多线程，则可能会发生崩溃和其他不可预测的错误。使用 Unity 的[作业系统](/engine/6000.0/manual/scripting/optimization/job-system.md)比使用自己的多线程更安全。作业系统使用多线程安全地并行执行作业，并且可以实现多线程的性能优势。有关更多信息，请参阅[作业系统概述](/engine/6000.0/manual/scripting/optimization/job-system/overview.md)。

### Awaitable 与作业系统相比##awaitable-compared-to-the-job-system

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

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

但是，不建议用于生命周期较短的操作，例如并行执行计算量大的算法。要充分利用多核 CPU 且并行执行算法，请改用[作业系统](/engine/6000.0/manual/scripting/optimization/job-system.md)。

## 从代码触发完成##triggering-completion-from-code

[`AwaitableCompletionSource`](/engine/6000.0/script-reference/unity-engine/awaitable-completion-source.md) 和 [`AwaitableCompletionSource<T>`](/engine/6000.0/script-reference/unity-engine/awaitable-completion-source-1.md) 允许创建从用户代码中引发完成的 `Awaitable` 实例。例如，这可用于实现用户提示，而无需实现状态机来等待用户交互完成：

```lang-csharp

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);
    }
}
```

## 其他资源##additional-resources

* [Awaitable 代码示例参考](/engine/6000.0/manual/scripting/optimization/async-await-support/async-awaitable-examples.md)
