编辑
2025-09-06
编程语言
00

目录

1. async 方法基础
2. await 的作用与状态机概览
3. 返回类型选择
4. 异常处理与取消
5. 阻塞等待的风险(Task.Wait() / .Result)
6. 状态机结构(示意)
7.UniTask(拓展)
7.1 基本用法
7.2 与 Task 的对比
7.3 指定 PlayerLoop 阶段
7.4 多线程场景
7.5 与协程的关系
7.6 组合操作示例

1. async 方法基础

  • 使用 async 修饰的方法为异步方法。
  • 常见返回类型:Task / Task<TResult> / void
  • 方法内可使用 await 表达式;若无 await,方法被视为同步返回已完成的 Task
csharp
public async Task<int> LoadDataAsync() { const int DelayMilliseconds = 1000; // 避免硬编码 await Task.Delay(DelayMilliseconds); return 42; }

2. await 的作用与状态机概览

  • 基本上需要搭配Task使用
  • await异步挂起当前方法,直到其等待的 Task 完成;完成后继续执行挂起处之后的代码;
  • 编译器把 async 方法转换为状态机(生成私有结构体/类,包含状态字段与 MoveNext());
  • 恢复执行默认在当前主线程继续执行

async_await_flowchart.png

3. 返回类型选择

  • Task:无返回值的异步方法;
  • Task<TResult>:有结果的异步方法;
  • async void:仅限事件处理器;无法被 await,异常也不易捕获,勿用于业务逻辑
csharp
public async Task UseAsyncVoidWrong() { // 反例:不要在业务逻辑里用 async void }

4. 异常处理与取消

  • 异常会聚合到返回的 Task 中,通过 await 抛出到调用点。
  • 取消使用 CancellationToken,由调用方传入。
csharp
public async Task DownloadAsync(CancellationToken ct) { try { await Task.Run(() => { // 模拟工作;实际代码里要定期检查 ct.IsCancellationRequested for (var i = 0; i < 3; i++) { ct.ThrowIfCancellationRequested(); System.Threading.Thread.Sleep(100); } }, ct); } catch (OperationCanceledException) { // 取消逻辑 throw; } }

5. 阻塞等待的风险(Task.Wait() / .Result

  • 在有 SynchronizationContext 的线程(如 UI/Unity 主线程)上阻塞等待可能导致死锁
  • 结论:在异步编程里用** await 贯穿到底**,不要混用阻塞等待,这样后续非常不利于代码的调试和维护
csharp
// 反例:可能死锁 // var r = SomeAsync().Result; // 或 SomeAsync().Wait();

6. 状态机结构(示意)

  • 编译器生成的状态机包含:
    • 状态字段(当前步骤索引)
    • Awaiter 字段(保存等待对象)
    • 局部变量字段(保持方法内局部变量)
    • MoveNext()(核心调度逻辑)
    • SetStateMachine()(调试器支持)

async_await_structure.png

7.UniTask(拓展)

  • UniTask 是由第三方库 Cysharp/UniTask 提供的异步工具包;
  • 与Task存在本质上的区别,他是一个Unity主线程的调度器,完全不涉及多线程,与协程类似,可以理解为更高级版本的协程
  • 目标:为 Unity 场景提供 更轻量、更高性能 的 Task 替代方案;
  • 主要优势:
    • 使用 struct 实现,避免 GC 分配;
    • async/await 语法完全兼容;
    • 支持 Unity PlayerLoop 阶段调度(Update、LateUpdate、FixedUpdate 等);
    • 提供比协程更现代、更灵活的组合操作;

7.1 基本用法

  • 与 Task 的使用方式几乎一致;
  • 常见返回类型:UniTask / UniTask<T>;
csharp
using Cysharp.Threading.Tasks; public class Example { public async UniTask LoadDataAsync() { await UniTask.Delay(1000); // 主线程异步等待 1 秒; UnityEngine.Debug.Log("完成"); } public async UniTask<int> GetNumberAsync() { await UniTask.Yield(); // 下一帧继续执行; return 42; } }

7.2 与 Task 的对比

  • Task 是引用类型,会产生 GC;
  • UniTask 是值类型(struct),大幅减少 GC 压力;
  • 默认运行在 Unity 主线程,除非显式切换到线程池;
特性TaskUniTask
类型class(引用类型);struct(值类型);
GC 分配有 GC 压力;几乎无 GC;
返回值Task / Task;UniTask / UniTask;
默认调度.NET 线程池 或 IOCP;Unity PlayerLoop(主线程);

7.3 指定 PlayerLoop 阶段

  • 可以指定在哪个 PlayerLoopTiming 阶段恢复执行;
  • 更精细地控制代码在 Unity 生命周期中的运行时机;
csharp
public async UniTask ExampleAsync() { UnityEngine.Debug.Log("帧1 Update 阶段"); await UniTask.Yield(PlayerLoopTiming.LateUpdate); // 下一帧 LateUpdate 阶段恢复; UnityEngine.Debug.Log("帧2 LateUpdate 阶段"); }

7.4 多线程场景

  • UniTask 默认不会开线程;
  • 若需要多线程,必须显式使用:
csharp
public async UniTask<int> ExampleMultiThreadAsync() { await UniTask.SwitchToThreadPool(); // 切到线程池; int result = HeavyCalculation(); // 子线程执行; await UniTask.SwitchToMainThread(); // 切回主线程; return result; }

7.5 与协程的关系

  • UniTask 可以看作是 更高级的协程调度器;
  • 相似点:都基于 Unity 的帧循环(PlayerLoop);
  • 不同点:
    • 协程依赖 IEnumerator,不支持返回值;
    • UniTask 基于 async/await,支持返回值和异常处理;

7.6 组合操作示例

  • WhenAll:并发执行多个任务并等待全部完成;
  • WhenAny:等待第一个任务完成;
csharp
public async UniTask ExampleWhenAll() { var task1 = UniTask.Delay(1000); var task2 = UniTask.Delay(2000); await UniTask.WhenAll(task1, task2); UnityEngine.Debug.Log("两个任务都完成"); }

本文作者:xuxuxuJS

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!