2.10 异步编程 (AsyncAwait)


文档摘要

2.10 异步编程 (Async/Await) 2.10 异步编程 (Async/Await) 在Unity游戏开发中,尤其是在C#脚本编程领域,异步编程是一项至关重要的技术。它允许我们编写非阻塞代码,从而保持游戏主线程的流畅运行,避免卡顿,提升用户体验。本章节将深入探讨C#中的异步编程模型,重点介绍 和 关键字,以及如何在Unity3D环境中有效地应用异步编程。 2.10.1 为什么需要异步编程? 在传统的同步编程模式下,代码按照顺序逐行执行。当遇到耗时操作,例如网络请求、文件读写、资源加载或复杂的计算时,程序会阻塞在这些操作上,直到操作完成才能继续执行后续代码。在Unity中,所有游戏逻辑、渲染、输入处理等都运行在主线程上。

2.10 异步编程 (Async/Await)

2.10 异步编程 (Async/Await)

在Unity游戏开发中,尤其是在C#脚本编程领域,异步编程是一项至关重要的技术。它允许我们编写非阻塞代码,从而保持游戏主线程的流畅运行,避免卡顿,提升用户体验。本章节将深入探讨C#中的异步编程模型,重点介绍 asyncawait 关键字,以及如何在Unity3D环境中有效地应用异步编程。

2.10.1 为什么需要异步编程?

在传统的同步编程模式下,代码按照顺序逐行执行。当遇到耗时操作,例如网络请求、文件读写、资源加载或复杂的计算时,程序会阻塞在这些操作上,直到操作完成才能继续执行后续代码。在Unity中,所有游戏逻辑、渲染、输入处理等都运行在主线程上。如果主线程被阻塞,游戏画面就会卡顿,用户输入无法及时响应,严重影响游戏体验。

异步编程旨在解决这个问题。它允许耗时操作在后台执行,而不会阻塞主线程。当后台操作完成时,程序会得到通知,并可以在主线程上继续处理结果。这使得游戏在执行耗时操作时仍然保持流畅和响应。

想象一下以下场景:

  1. 加载大型场景或资源: 同步加载会阻塞主线程,导致游戏卡顿,出现明显的加载停顿。

  2. 网络请求: 例如从服务器下载玩家数据、排行榜信息等。同步网络请求会阻塞主线程,如果网络不稳定,可能导致长时间卡顿。

  3. 复杂的计算任务: 例如AI寻路、物理模拟等。如果计算量过大,同步执行会占用主线程大量时间,导致帧率下降。

在这些场景中,异步编程就显得尤为重要。它可以将这些耗时操作放到后台执行,让主线程继续处理游戏逻辑和渲染,从而保证游戏的流畅性。

2.10.2 同步 vs. 异步:概念对比

为了更好地理解异步编程,我们先来对比一下同步和异步的概念:

同步 (Synchronous)

  • 定义: 任务按顺序执行,一个任务完成后才能开始下一个任务。

  • 特点:

    • 阻塞: 当执行耗时操作时,当前线程会被阻塞,等待操作完成。

    • 顺序执行: 代码按照编写顺序依次执行。

    • 简单直观: 易于理解和编写,符合传统的编程思维。

异步 (Asynchronous)

  • 定义: 任务可以并发执行,一个任务的执行不必等待另一个任务完成。

  • 特点:

    • 非阻塞: 当执行耗时操作时,当前线程不会被阻塞,可以继续执行其他任务。

    • 并发执行: 多个任务可以同时或交替执行。

    • 提高效率和响应性: 能够更好地利用系统资源,提高程序的响应速度。

    • 代码结构可能更复杂: 需要处理回调、Promise、Async/Await等机制来管理异步操作的结果。

可以用一个简单的生活例子来比喻:

同步: 你去餐厅点餐,需要一直站在柜台前等待服务员完成你的订单,直到拿到餐点才能离开。在这个过程中,你什么都不能做,只能等待。

异步: 你去餐厅点餐,点完餐后服务员给你一个号码牌,你可以先去座位上休息或者做其他事情,当你的餐点准备好后,服务员会叫号通知你,你再去取餐。在这个过程中,你不需要一直等待,可以同时做其他事情。

在Unity游戏中,主线程就像餐厅的柜台,同步操作会阻塞柜台(主线程),导致其他顾客(游戏逻辑、渲染)无法得到服务。而异步操作则允许我们像拿到号码牌一样,将耗时操作放到后台处理,让主线程可以继续服务其他任务,提升整体效率和用户体验。

2.10.3 C# Async/Await:异步编程的利器

C# 提供了 asyncawait 关键字,极大地简化了异步编程的复杂性。它们基于 Task-based Asynchronous Pattern (TAP),提供了一种更加简洁、易读、易维护的异步编程模型。

核心概念:

  • async 关键字: 用于修饰方法、Lambda 表达式或匿名方法,表明该方法是异步方法。

    • 异步方法内部可以使用 await 关键字。

    • 异步方法必须返回 TaskTask<T>void。通常推荐返回 TaskTask<T>,以便更好地管理异步操作的结果和异常。返回 void 的异步方法主要用于事件处理程序,需要谨慎使用。

  • await 关键字: 只能在 async 方法内部使用。

    • await 运算符用于等待一个异步操作完成。

    • 当程序执行到 await 表达式时,会暂停当前异步方法的执行,并将控制权返回给调用者(通常是主线程)。

    • await 等待的异步操作完成时,程序会自动回到 await 表达式的断点处,继续执行异步方法的后续代码。

    • await 表达式不会阻塞当前线程(通常是主线程),而是异步地等待操作完成。

  • TaskTask<T> Task 类表示一个异步操作,可以理解为“未来”的结果。

    • Task 代表不返回值的异步操作。

    • Task<T> 代表返回类型为 T 的异步操作。

    • 可以使用 Task.Run()Task.Delay()UnityWebRequestAsyncOperation 等方法创建和启动异步任务。

    • 可以使用 await 运算符等待 TaskTask<T> 完成,并获取其结果(对于 Task<T>)。

工作原理 (Graph TD 图示):

文字解释 Graph TD 图示流程:

  1. 开始执行 Async 方法 (A): 程序开始执行标记为 async 的方法。

  2. 遇到 await 表达式? (B): 在异步方法执行过程中,程序会检查是否遇到 await 表达式。

  3. Yes (遇到 await):

    • 暂停 Async 方法执行 (C): 如果遇到 await 表达式,当前异步方法的执行会暂停。

    • 将控制权返回给调用者 (D): 程序会将控制权返回给调用者,通常是主线程。主线程可以继续处理其他任务,例如渲染、用户输入等。

    • 异步操作在后台执行 (E): await 表达式等待的异步操作会在后台线程中继续执行,例如网络请求、文件读写等。

  4. No (没有遇到 await):

    • 继续同步执行 Async 方法 (F): 如果没有遇到 await 表达式,异步方法会像普通的同步方法一样继续执行。
  5. 异步操作完成? (G): 后台执行的异步操作完成时,会通知程序。

  6. Yes (异步操作完成):

    • 恢复 Async 方法执行 (H): 程序会恢复之前暂停的异步方法的执行。

    • 从 await 断点继续执行 (I): 异步方法会从 await 表达式的断点处继续执行后续代码。

  7. No (异步操作未完成): 如果异步操作尚未完成,程序会继续等待 (回到步骤 E)。

  8. Async 方法执行结束 (J): 异步方法最终执行完毕。

总结 Async/Await 的优点:

  • 简化异步代码: 使用 asyncawait 关键字,可以将异步代码写得像同步代码一样,大大提高了代码的可读性和可维护性。

  • 避免回调地狱: 传统的异步编程往往需要使用回调函数来处理异步操作的结果,容易形成“回调地狱”,代码难以理解和维护。Async/Await 有效地避免了回调地狱。

  • 异常处理更方便: 可以使用 try-catch 块来捕获异步方法中的异常,与同步代码的异常处理方式一致。

  • 提高代码效率和响应性: 通过异步执行耗时操作,保持主线程的流畅运行,提升游戏的响应速度和用户体验。

2.10.4 Unity 中的 Async/Await 代码实践

在 Unity 中,Async/Await 可以应用于各种需要异步操作的场景。以下是一些常见的代码实践示例:

示例 1: 延迟执行 (模拟等待效果)

using System.Collections; using System.Threading.Tasks; using UnityEngine; using UnityEngine.UI; public class AsyncAwaitExample : MonoBehaviour { public Text messageText; public async void Start() // 注意这里使用了 async void,仅用于 MonoBehaviour 的事件方法 { messageText.text = "开始等待..."; await Task.Delay(3000); // 异步等待 3 秒 (3000 毫秒) messageText.text = "等待结束!"; } }

代码解释:

  • async void Start(): Start() 方法被标记为 async void。在 MonoBehaviour 的事件方法中,通常可以使用 async void,但需要注意异常处理,因为 async void 方法中抛出的异常难以被外部捕获。

  • await Task.Delay(3000);: Task.Delay(3000) 创建一个表示延迟 3000 毫秒的 Task。await 关键字等待这个 Task 完成,但不会阻塞主线程。在等待期间,Unity 可以继续处理渲染、输入等。3 秒后,程序会回到 await 表达式的断点处,继续执行 messageText.text = "等待结束!";

运行效果:

游戏开始运行时,UI 文本首先显示 "开始等待...",然后程序会异步等待 3 秒,期间游戏画面不会卡顿,3 秒后 UI 文本更新为 "等待结束!"。

示例 2: 异步加载资源 (AssetBundle)

using System.Collections; using System.Threading.Tasks; using UnityEngine; using UnityEngine.UI; public class AsyncAssetBundleLoad : MonoBehaviour { public string bundleURL = "http://your-server/assetbundle/mybundle"; public string assetName = "MyTexture"; public RawImage rawImage; public Text messageText; public async void Start() { messageText.text = "开始下载 AssetBundle..."; AssetBundleCreateRequest asyncLoadAssetBundleCreateRequest = AssetBundle.LoadAsync(bundleURL); await asyncLoadAssetBundleCreateRequest; // 异步等待 AssetBundle 下载完成 AssetBundle assetBundle = asyncLoadAssetBundleCreateRequest.assetBundle; if (assetBundle == null) { Debug.LogError("Failed to load AssetBundle!"); messageText.text = "AssetBundle 加载失败!"; return; } messageText.text = "AssetBundle 下载完成,加载纹理..."; AssetBundleRequest assetLoadRequest = assetBundle.LoadAssetAsync<Texture2D>(assetName); await assetLoadRequest; // 异步等待纹理加载完成 Texture2D texture = assetLoadRequest.asset as Texture2D; if (texture == null) { Debug.LogError("Failed to load Texture!"); messageText.text = "纹理加载失败!"; assetBundle.Unload(true); // 卸载 AssetBundle return; } rawImage.texture = texture; messageText.text = "纹理加载完成,显示在 RawImage 上!"; assetBundle.Unload(false); // 卸载 AssetBundle,保留已加载的资源副本 } }

代码解释:

  • AssetBundle.LoadAsync(bundleURL): 使用 AssetBundle.LoadAsync 方法异步加载 AssetBundle。该方法返回 AssetBundleCreateRequest 对象,表示异步加载请求。

  • await asyncLoadAssetBundleCreateRequest;: await 关键字等待 AssetBundle 下载完成。下载过程在后台进行,不会阻塞主线程。

  • assetBundle.LoadAssetAsync<Texture2D>(assetName): 使用 assetBundle.LoadAssetAsync 方法异步加载 AssetBundle 中的纹理资源。

  • await assetLoadRequest;: await 关键字等待纹理资源加载完成。

  • 资源加载完成后,将纹理赋值给 RawImage 组件显示。

  • 最后卸载 AssetBundle,根据需求选择 Unload(true) (卸载所有资源) 或 Unload(false) (卸载 AssetBundle 但保留已加载的资源副本)。

运行效果:

游戏开始运行时,会异步下载 AssetBundle 和加载纹理资源,期间游戏画面保持流畅。下载和加载完成后,纹理会显示在 RawImage 组件上。

示例 3: 异步网络请求 (UnityWebRequest)

using System.Collections; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; public class AsyncWebRequest : MonoBehaviour { public string requestURL = "https://api.example.com/data"; // 替换为实际 API 地址 public Text responseText; public Text messageText; public async void Start() { messageText.text = "发送网络请求..."; UnityWebRequest www = UnityWebRequest.Get(requestURL); UnityWebRequestAsyncOperation asyncOperation = www.SendWebRequest(); await asyncOperation; // 异步等待网络请求完成 if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.ProtocolError) { Debug.LogError("网络请求错误: " + www.error); responseText.text = "网络请求失败: " + www.error; messageText.text = "网络请求失败!"; } else { Debug.Log("网络请求成功: " + www.downloadHandler.text); responseText.text = www.downloadHandler.text; // 显示服务器返回的文本数据 messageText.text = "网络请求成功,数据已显示!"; } www.Dispose(); // 释放 UnityWebRequest 资源 } }

代码解释:

  • UnityWebRequest.Get(requestURL): 创建一个 GET 请求的 UnityWebRequest 对象。

  • www.SendWebRequest(): 发送网络请求,返回 UnityWebRequestAsyncOperation 对象,表示异步网络请求操作。

  • await asyncOperation;: await 关键字等待网络请求完成。网络请求在后台进行,不会阻塞主线程。

  • 请求完成后,检查 www.result 判断请求是否成功,处理错误或显示服务器返回的数据。

  • 最后使用 www.Dispose() 释放 UnityWebRequest 对象,避免资源泄露。

运行效果:

游戏开始运行时,会异步发送网络请求到指定的 URL,期间游戏画面保持流畅。网络请求完成后,会将服务器返回的数据显示在 UI 文本组件上。

2.10.5 异步编程的最佳实践和注意事项

  • 只在必要时使用异步: 异步编程主要用于处理耗时操作,避免阻塞主线程。对于简单的、非耗时的操作,没有必要使用异步。

  • 避免 async void (除非是事件处理程序): async void 方法返回 void,无法被 await,并且异常难以捕获。通常应该返回 TaskTask<T>。只有在 MonoBehaviour 的事件方法中,由于 Unity 的事件系统限制,才可能使用 async void,但需要谨慎处理异常。

  • 合理使用 Task.Run() Task.Run() 可以将 CPU 密集型任务放到线程池中执行,避免阻塞主线程。但需要注意线程安全问题,以及线程切换的开销。对于 Unity 主线程相关的操作(例如访问 Unity API),仍然需要在主线程上执行。可以使用 UnityMainThreadDispatcher 等工具将结果切换回主线程。

  • 处理异步操作的取消和超时: 对于长时间运行的异步操作,应该考虑提供取消机制,例如使用 CancellationToken。也可以设置超时时间,避免无限等待。

  • 异常处理: 使用 try-catch 块捕获异步方法中的异常,保证程序的健壮性。

  • 性能考虑: 虽然异步编程可以提高程序的响应性,但也可能引入额外的开销,例如线程切换、上下文切换等。需要根据实际情况进行性能评估和优化。

  • 代码清晰和可维护性: 使用 Async/Await 编写异步代码时,要注重代码的清晰度和可维护性。合理的命名、注释和代码结构能够提高代码的可读性。

2.10.6 总结

C# 的 Async/Await 异步编程模型为 Unity 游戏开发带来了极大的便利。它简化了异步代码的编写,提高了游戏程序的响应性和用户体验。通过合理地应用 Async/Await,我们可以轻松地处理各种耗时操作,例如资源加载、网络请求、复杂计算等,而不会阻塞主线程,从而打造更加流畅、高效的游戏。

本章节深入探讨了异步编程的概念、Async/Await 的核心原理、以及在 Unity 中的代码实践。希望通过学习本章节内容,您能够掌握 Async/Await 异步编程技术,并在实际的 Unity 项目开发中灵活运用,提升您的游戏开发技能。


发布者: 作者: 转发
评论区 (0)
U