Awaitable 完成和延续
Understand how asynchronous code resumes on completion of an awaited task and how this affects the function and performance of your application.
阅读时间8 分钟最后更新于 1 个月前
awaitTaskAwaitable.NET Task 延续
开始等待时所处的状态代码相关信息称为同步上下文。.NET 平台提供了用于捕获此类信息的 类。 延续将在调用异步方法的同步上下文中运行,如果未设置同步上下文,则通过线程池运行。
SynchronizationContextTask大多数 Unity API 都不是线程安全的,只能从主线程调用。因此,Unity 会使用自定义 覆盖默认 ,以确保所有 .NET 延续在编辑模式和运行模式下都默认在主线程上运行。如果从 Unity 主线程调用 返回方法,则延续将发布到 ,并在主线程上的下一帧 刷新时运行。如果从后台线程调用,则会在线程池线程上完成。
UnitySynchronizationContextSynchronizationContextTaskTaskUnitySynchronizationContextUpdate捕获同步上下文会增加应用程序的性能开销,而在主线程上等待下一帧恢复更新会引入大规模延迟。您可以改用 来避免这两个问题。
AwaitableAwaitable 延续
除非另有说明,否则 Unity API 返回的所有 实例都在主线程上完成,因此无需捕获同步上下文。 延续将在操作完成后同步运行,这意味着代码可以立即恢复,而不必等待下一帧。
AwaitableAwaitable您可以使用 显式重载默认延续行为。以下示例指定了 在后台线程上完成:
Awaitable.BackgroundThreadAsyncAwaitableprivate 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}
在这种情况下,将 设置为在后台线程上完成会产生问题。在后台线程上恢复 方法时,对 的下一次调用会失败,因为 仅在主线程上有效。
AwaitableStartSceneManager.LoadSceneAsyncSceneManager.LoadSceneAsync要解决这种情况,可以使用 使 返回方法默认在主线程上完成,同时仍然提供后台线程工作的选项。在以下示例中,默认情况下, 在主线程上完成之前会在后台线程上做一些工作。 参数允许调用者在适当时在后台线程上完成,例如在链接繁重的计算操作时:
Awaitable.MainThreadAsyncAwaitableDoHeavyComputationInBackgroundAsynccontinueOnMainThreadprivate 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如果从主线程调用 返回 API,但未同步完成,则需要至少等待下一次 刷新(33 毫秒,30fps),延续才能运行。如果担心网络延迟,建议在主线程之外执行此操作,并使用自定义逻辑在主线程和网络任务之间同步。
TaskUpdate在开发构建中,如果尝试在多线程代码中使用 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>Awaitablepublic 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); }}