如何实现一个可以用 await 异步等待的 Awaiter

本文介绍了如何在.NET和C#中实现一个适用于UI的可等待类型DispatcherAsyncOperation,以解决UI的异步等待问题。通过定义Awaiter接口和实现自定义Awaitable,文章详细展示了如何避免在后台线程等待UI操作,减少资源浪费。文章提供完整代码示例,包括创建后台线程创建UI控件的功能,以及如何在不同场景下使用DispatcherAsyncOperation。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

.NET 和 C# 共同给我们带来的 async/await 异步编程模型(TAP)用起来真的很爽。为了实现异步等待,我们只需要在一切能够能够异步等待的方法前面加上 await 即可。能够异步等待的最常见的类型莫过于 Task,但也有一些其他类型。即便有些耗时操作没有返回可等待的类型,我们也可以用一句 Task.Run(action) 来包装(同步转异步 - 林德熙 中也有说明);不过副作用就是 Run 里面的方法在后台线程执行了(谁知道这是好处呢还是坏处呢 ^_^)。

问题就在于,有些“耗时”操作根本就无法放入后台线程,典型的莫过于“耗时”的 UI 操作。本文将通过实现一个适用于 UI 的可等待类型来解决这种 UI 的“耗时”等待问题。


本文代码较多,阅读建议:

  1. 标注为“本文推荐的完整代码”的代码块可直接放入自己的项目中使用,也贴出了 GitHub 上我以 MIT 开源的源代码(可能 GitHub 上会经常更新)。
  2. 标注“此处为试验代码”的代码块表明此处代码并不完善,仅用于本文分析使用,不建议放到自己的项目中使用。
  3. 没有注释标注的代码块是用于研究的代码片段,不需要使用。
  4. 可点击下面的导航跳转到你希望的地方。

我们的需求

这里说的 UI “耗时”,“耗时”打了引号,是因为严格来说并不是真的卡死了 UI,而是某个函数的执行需要更多的 UI 操作才能继续。这句话可能比较难懂,但举两个例子就好懂了。

  1. 某个函数的执行需要显示一个用户控件,用户填写控件中的信息并确定后,函数才继续执行。这种感觉很像模态窗口,但我们却是在同一个窗口内实现,不能通过模态窗口来实现我们的功能。(UWP 中的 ContentDialog 就是这么干的。)
  2. 我们需要在后台线程创建一个控件,创建完毕之后在原线程返回。这样我们就能得到一个在后台线程创建的控件了。

本文将以实现第 2 条为目标,一步步完善我们的代码,并做出一个非常通用的 UI 可等待类出来。最终你会发现,我们的代码也能轻松应对第 1 条的需求。

什么样的类是可等待的?

我们已经知道 Task 是可等待的,但是去看看 Task 类的实现,几乎找不到哪个基类、接口或者方法属性能够告诉我们与 await 相关。所以,await 的实现可能是隐式的。

幸运的是,Dixin’s Blog - Understanding C# async / await (2) The Awaitable-Awaiter Pattern 一文解决了我们的疑惑。async/await 是给编译器用的,只要我们的类包含一个 GetAwaiter 方法,并返回合适的对象,我们就能让这个类的实例被 await 使用了。

既然需要一个 GetAwaiter 方法,那我们先随便写个方法探索一下:

Test DoAsync()
{
    return new Test();
}
class Test
{
   
   
    void GetAwaiter()
    {
    }
}

尝试调用:

await DoAsync();

编译器告诉我们:

Test.GetAwaiter() 不可访问,因为它具有一定的保护级别。

原来 GetAwaiter 方法需要是可以被调用方访问到的才行。

于是我们将 GetAwaiter 前面的访问修饰符改成 public。现在提示变成了:

await 要求类型 Test 包含适当的 GetAwaiter 方法。

考虑到一定要获取到某个对象才可能有用,于是我们返回一个 Test2 对象:

public class Test
{
    public Test2 GetAwaiter()
    {
        return new Test2();
    }
}

public class Test2
{
}

这时编译器又告诉我们:

Test2 未包含 IsCompleted 的定义。

加上 public bool IsCompleted { get; },编译器又说:

Test2 不实现 INotifyCompletion。

于是我们实现之,编译器又告诉我们:

Test2 未包含 GetResult 的定义。

于是我们加上一个空的 GetResult 方法,现在编译器终于不报错了。

现在我们一开始的 DoAsync 和辅助类型变成了这样:

// 注:此处为试验代码。
private Test DoAsync()
{
    return new Test();
}

public class Test
{
    public Test2 GetAwaiter()
    {
        return new Test2();
    }
}

public class Test2 : INotifyCompletion
{
    public bool IsCompleted { get; }
    public void GetResult() { }
    public void OnCompleted(Action continuation) { }
}

总结起来,要想使一个方法可被 await 等待,必须具备以下条件:

  1. 这个方法返回一个类 A 的实例,这个类 A 必须满足后面的条件。
  2. 此类 A 有一个可被访问到的 GetAwaiter 方法(扩展方法也行,这算是黑科技吗?),方法返回类 B 的实例,这个类 B 必须满足后面的条件;
  3. 此类 B 实现 INotifyCompletion 接口,且拥有 bool IsCompleted { get; } 属性、GetResult() 方法、void OnCompleted(Action continuation) 方法。

定义抽象的 Awaiter/Awaitable

这里我们发现一个神奇的现象——明明那些属性和方法都是不可缺少的,却并没有接口来约束它们,而是靠着编译器来约束。

然而作为团队开发者的一员,我们不可能让每一位开发者都去探索一遍编译器究竟希望我们怎么来实现 await,于是我们自己来定义接口。方便我们自己后续再实现自己的可等待类型。

以下接口在 Dixin’s Blog - Understanding C# async / await (2) The Awaitable-Awaiter Pattern 一文中已有原型;但我增加了更通用却更严格的泛型约束,使得这些接口更加通用,且使用者实现的过程中更加不容易出错。

// 此段代码为本文推荐的完整版本。
// 可复制或前往我的 GitHub 页面下载:
// https://git
<think>好的,我现在需要详细介绍一下C#async/await在Unity中的异步使用,并且要多举几个例子来说明。首先,我得回忆一下asyncawait的基本概念,以及它们在Unity中的应用场景。 首先,asyncawaitC#中用于简化异步编程的关键字。在Unity中,异步操作非常常见,比如加载资源、网络请求、延迟执行等。以前可能用协程(Coroutine)来处理这些,但async/await提供了更直观的写法,特别是对于有C#背景的开发者来说可能更熟悉。 接下来,我需要明确几个重点:语法结构、在Unity中的使用注意事项、优点以及具体的例子。可能还需要对比协程和async/await的区别,帮助用户理解为什么选择后者。 然后,例子部分需要涵盖不同的场景。比如加载场景、等待时间、网络请求、同时处理多个异步任务等。每个例子都要说明如何使用async/await,并且指出需要注意的地方,比如是否需要使用Unity的主线程,如何避免阻塞等。 另外,Unity对C#版本的支持情况也需要考虑。Unity的版本不同,可能支持的C#版本也不同,async/await需要一定的C#版本支持,比如C# 5.0及以上。可能需要提醒用户检查他们的Unity版本是否支持,或者是否需要调整编译环境。 同时,要提到在Unity中使用async/await时,涉及到主线程的问题。Unity的API大部分需要在主线程执行,所以在异步方法中调用Unity的API时,要确保上下文是否正确。例如,在后台线程中修改GameObject的属性会引发错误,可能需要回到主线程,但使用async/await默认情况下会在主线程继续执行,这可能是一个优势。 还需要注意,Unity中的一些异步操作可能需要使用特定的Async方法,比如UnityWebRequest.SendWebRequest()返回的是一个AsyncOperation,可能需要适配为Task,以便用await。或者使用TaskCompletionSource来封装旧的异步操作为Task。 另外,CancellationToken的使用也是一个重要点,如何在Unity中处理取消异步操作,比如当场景切换或对象销毁时取消正在进行的异步任务,避免内存泄漏或错误。 现在,我需要组织这些内容,分成几个部分,每个部分配以例子。可能的结构如下: 1. 基本语法和结构:如何在Unity中声明async方法,使用await。 2. 例子一:等待一段时间,比如延迟执行。 3. 例子二:异步加载资源或场景。 4. 例子三:处理多个异步任务,比如同时加载多个资源。 5. 例子四:网络请求,使用UnityWebRequest配合async/await。 6. 注意事项:线程上下文、取消操作、兼容性等。 每个例子需要代码示例,并解释关键点。比如,在例子一中,使用Task.Delay来替代协程中的WaitForSeconds。不过要注意,Task.Delay使用的是系统时间,而WaitForSeconds受游戏时间缩放影响,可能需要使用Unity的等待方式,比如使用AsyncOperation或者自定义的等待方法。 可能需要使用Unity提供的AsyncOperation的扩展方法,比如GetAwaiter(),或者使用UniTask这样的第三方库来更好地集成Unity的异步操作。不过用户的问题可能更倾向于使用原生的async/await,而不是第三方库。 例如,UnityWebRequest.SendWebRequest()返回的是一个AsyncOperation,可以await,但可能需要配置成允许异步完成。或者使用Task.Run来包装,但需要注意线程问题。 另外,Unity中的MonoBehaviour生命周期方法是否可以标记为async?比如在Start或Update方法中使用await。根据我的知识,这应该是允许的,但需要注意如果方法被标记为async void,可能会导致异常无法捕获,所以最好使用async Task,并在调用时处理异常。 总结下来,步骤应该是: - 介绍async/await的基本用法。 - 提供多个具体例子,涵盖不同场景。 - 指出常见问题和注意事项。 - 比较与协程的异同,说明各自的适用场景。 现在开始组织内容,确保每个例子正确,并符合Unity的实际应用。例如: 例子一:延迟执行。使用Task.Delay,但要注意游戏的时间缩放。或者使用自定义的等待方法,比如将秒转换为毫秒,并考虑Time.timeScale。或者使用Unity的WaitForSeconds,但如何用await等待它?可能需要使用UniTask库,或者自己封装一个Awaiter。如果用户不想用第三方库,可能需要用Task.Delay结合Time.deltaTime来计算,但可能不够准确。 例如: public async void Start() { await Task.Delay(1000); // 等待1秒,但受系统时间影响,游戏暂停时也会继续 } 但这样可能不符合游戏暂停的需求。所以可能需要使用协程的WaitForSeconds,或者另一种方式: public async void Start() { float duration = 1f; float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; await Task.Yield(); } // 执行后续代码 } 但这样写的话,需要手动处理循环,可能不如协程方便。这时候可能需要使用UniTask库的Delay方法,或者直接使用协程。 这说明在某些情况下,原生async/await可能不如协程方便,或者需要额外处理。但用户的问题是如何用async/await,所以需要找到合适的方式。 另一个例子,加载场景: using UnityEngine.SceneManagement; using System.Threading.Tasks; public async void LoadSceneAsync(string sceneName) { AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); asyncLoad.allowSceneActivation = false; while (!asyncLoad.isDone) { float progress = Mathf.Clamp01(asyncLoad.progress / 0.9f); Debug.Log("加载进度: " + (progress * 100) + "%"); if (asyncLoad.progress >= 0.9f) { asyncLoad.allowSceneActivation = true; } await Task.Yield(); } } 这里用await Task.Yield()来每帧等待,类似于协程中的yield return null。这种方法可能有效,但需要手动循环检查进度。 或者,可以封装AsyncOperation为Task: public static class AsyncOperationExtensions { public static Task AsTask(this AsyncOperation asyncOp) { var tcs = new TaskCompletionSource<bool>(); asyncOp.completed += _ => tcs.SetResult(true); return tcs.Task; } } 然后可以这样使用: public async Task LoadSceneAsync(string sceneName) { AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); await asyncLoad.AsTask(); Debug.Log("场景加载完成"); } 这样更简洁,但需要自己写扩展方法。这种方法可能更符合async/await的风格。 例子三:同时加载多个资源,使用Task.WhenAll: public async Task LoadMultipleTextures(string[] paths) { List<Task<Texture>> tasks = new List<Task<Texture>>(); foreach (var path in paths) { tasks.Add(LoadTextureAsync(path)); } Texture[] textures = await Task.WhenAll(tasks); Debug.Log("所有贴图加载完成"); } private async Task<Texture> LoadTextureAsync(string path) { UnityWebRequest request = UnityWebRequestTexture.GetTexture(path); await request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { return DownloadHandlerTexture.GetContent(request); } else { throw new System.Exception("加载失败: " + request.error); } } 这里需要注意UnityWebRequest的SendWebRequest返回的是AsyncOperation,所以需要await,但可能需要使用扩展方法,或者直接使用.SendWebRequest().AsTask(),如果已经封装了扩展方法的话。 另一个例子,网络请求: public async Task<string> FetchDataFromServer(string url) { using (UnityWebRequest webRequest = UnityWebRequest.Get(url)) { await webRequest.SendWebRequest(); if (webRequest.result == UnityWebRequest.Result.ConnectionError || webRequest.result == UnityWebRequest.Result.ProtocolError) { Debug.LogError("错误: " + webRequest.error); return null; } else { return webRequest.downloadHandler.text; } } } 这样封装后,可以在其他方法中await这个FetchDataFromServer。 需要注意,UnityWebRequest在using语句中,确保资源释放。另外,处理错误的情况。 关于取消操作,可以使用CancellationTokenSource: private CancellationTokenSource cts; public async void StartDownload() { cts = new CancellationTokenSource(); try { string data = await FetchDataFromServer("http://example.com", cts.Token); Debug.Log(data); } catch (TaskCanceledException) { Debug.Log("下载被取消"); } } public void CancelDownload() { cts?.Cancel(); } 在FetchDataFromServer方法中需要传递CancellationToken,并在适当的时候检查是否取消: public async Task<string> FetchDataFromServer(string url, CancellationToken cancellationToken) { using (UnityWebRequest webRequest = UnityWebRequest.Get(url)) { var asyncOp = webRequest.SendWebRequest(); while (!asyncOp.isDone) { cancellationToken.ThrowIfCancellationRequested(); await Task.Yield(); } // 处理结果 } } 或者,可以将异步操作封装为Task,并在其中注册取消: public static class AsyncOperationExtensions { public static Task AsTask(this AsyncOperation asyncOp, CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource<bool>(); asyncOp.completed += _ => tcs.SetResult(true); cancellationToken.Register(() => tcs.TrySetCanceled()); return tcs.Task; } } 这样,在调用时可以传递CancellationToken: await webRequest.SendWebRequest().AsTask(cancellationToken); 这样在取消时,会触发Task取消。 最后,注意事项部分: - Unity的大部分API需要在主线程调用,async/await默认会回到原上下文,因此在Unity中,await之后的代码通常会在主线程执行,这使得可以直接操作Unity对象,无需担心线程问题。 - 避免async void,除非是事件处理(如按钮点击),因为async void无法捕获异常,容易导致崩溃。应尽量使用async Task。 - 处理取消,避免内存泄漏和无效操作。 - Unity版本和.NET版本的支持,确保项目设置中的API Compatibility Level支持.NET 4.x或更高,以便使用async/await。 - 性能考虑,频繁创建和销毁Task可能带来开销,需合理使用。 总结后,整理这些内容,分步骤说明,并提供代码示例,同时指出常见问题和最佳实践。</think>### C#async/await在Unity中的异步使用详解 #### 一、基本概念与语法 `async/await`是C#中简化异步编程的关键字,通过它们可以编写更直观的异步代码。在Unity中,常用于处理资源加载、网络请求、延迟操作等场景。核心规则: - `async`标记方法为异步方法。 - `await`等待异步操作完成,期间释放线程控制权。 - 返回类型为`Task`或`Task<T>`(推荐),避免`async void`。 #### 二、Unity中的使用示例 ##### 示例1:等待固定时间(受游戏时间缩放影响) ```csharp public async Task DelayAsync(float seconds) { float start = Time.time; while (Time.time - start < seconds) { await Task.Yield(); // 每帧恢复,类似协程 } Debug.Log("等待结束"); } // 调用 async void Start() { await DelayAsync(3f); Debug.Log("3秒后执行"); } ``` ##### 示例2:异步加载场景 ```csharp using UnityEngine.SceneManagement; using System.Threading.Tasks; public async Task LoadSceneAsync(string sceneName) { AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); asyncLoad.allowSceneActivation = false; // 控制是否自动激活场景 while (!asyncLoad.isDone) { float progress = asyncLoad.progress / 0.9f; // Unity加载到0.9会暂停 Debug.Log($"加载进度: {progress * 100}%"); if (progress >= 1f) { asyncLoad.allowSceneActivation = true; } await Task.Yield(); // 每帧检查一次 } } ``` ##### 示例3:并行加载多个资源 ```csharp public async Task LoadAllTexturesAsync(string[] urls) { List<Task<Texture>> tasks = new List<Task<Texture>>(); foreach (var url in urls) { tasks.Add(LoadTextureAsync(url)); } Texture[] textures = await Task.WhenAll(tasks); Debug.Log($"共加载 {textures.Length} 张贴图"); } private async Task<Texture> LoadTextureAsync(string url) { using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(url)) { await request.SendWebRequest(); // UnityWebRequest的异步操作 if (request.result == UnityWebRequest.Result.Success) { return DownloadHandlerTexture.GetContent(request); } else { throw new System.Exception($"加载失败: {request.error}"); } } } ``` ##### 示例4:取消异步操作 ```csharp private CancellationTokenSource _cts; public async void StartDownload() { _cts = new CancellationTokenSource(); try { string data = await FetchDataAsync("https://api.example.com", _cts.Token); Debug.Log(data); } catch (TaskCanceledException) { Debug.Log("请求被取消"); } } public void Cancel() { _cts?.Cancel(); // 触发取消 } private async Task<string> FetchDataAsync(string url, CancellationToken token) { using (UnityWebRequest request = UnityWebRequest.Get(url)) { var asyncOp = request.SendWebRequest(); token.Register(() => request.Abort()); // 取消时终止请求 await asyncOp; // 直接等待UnityWebRequest的AsyncOperation return request.downloadHandler.text; } } ``` #### 三、注意事项 1. **线程安全**:Unity API需在主线程调用,`async/await`默认在调用上下文恢复(通常是主线程)。 2. **避免阻塞**:不要在异步方法中调用`.Result`或`.Wait()`,会导致死锁。 3. **错误处理**:用`try-catch`捕获异步异常,避免未处理异常导致崩溃。 4. **性能优化**:频繁创建`Task`可能影响性能,建议复用或使用对象池。 5. **版本兼容**:需在Unity Player Settings中设置`.NET 4.x`或`.NET Standard 2.1`。 #### 四、与协程(Coroutine)的对比 | 特性 | async/await | 协程 | |--------------------|--------------------------------------|-------------------------------| | **语法简洁性** | 更直观,类似同步代码 | 需要`yield return`和迭代器 | | **返回值处理** | 支持`Task<T>`直接返回结果 | 需通过回调或全局变量传递 | | **取消支持** | 原生支持`CancellationToken` | 需手动判断`StopCoroutine` | | **多任务并行** | 通过`Task.WhenAll`轻松实现 | 需复杂的手动管理 | #### 五、总结 `async/await`在Unity中提供了更现代化的异步编程方式,尤其适合复杂逻辑和并行任务。结合`CancellationToken`和`Task.WhenAll`等特性,可以高效管理资源加载、网络请求等场景。但需注意Unity的API线程限制和版本兼容性。
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值