async异步方案
async 是c#后面出的异步解决方案,不同于 yield 的生产者消费者模式
async 是完整的异步方案,包括异常处理,返回值处理等
async 的实现原理是编译器处理 async ,封装成状态机,配合 await 操作符注入回调
async/await的标准写法
// 一般要传入 cancelToken
// 正如事件监听器一样,异步开始时注册监听器,取消时要反注册监听器
// 而async取消操作是由 CancellationToken 完成
public async Task TestAsync(CancellationToken cancelToken)
{
try
{
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
// OperationCanceledException 是取消操作抛出的异常,一般不处理
// 只需要在 finally 中清理资源即可,最外层也会忽略该异常,不会报错
}
finally
{
// 当异步正常完成或取消后,在这里清理资源
}
// 如果整个函数中没有任何 await 执行,则编译器会发出警告,可以加入下面这句来避免警告
// 有返回值的可以用 await Task.FromResult(ret);
await Task.CompletedTask;
}
// 调用方标准写法
// 最外层由同步函数转异步函数,只能使用 async void
CancellationTokenSource m_cancelTokenSource;
public async void Execute()
{
// 创建一个可取消的源
m_cancelTokenSource = new CancellationTokenSource();
await TestAsync(cancelTokenSource.Token);
}
// 取消异步操作
public void Cancel()
{
m_cancelTokenSource.Cancel();
}
取消任务
-
所有任务都接受1个 CancellationToken 参数用来取消,你的async或自定义任务设计时也要这样
1个token可以被多个await使用var cts = new CancellationTokenSource(); cancelButton.onClick.AddListener(() => { cts.Cancel(); }); await UnityWebRequest.Get("http://google.co.jp").SendWebRequest().WithCancellation(cts.Token); await UniTask.DelayFrame(1000, cancellationToken: cts.Token);
-
你还可以通过 MonoBehaviour 或 GameObject 对象扩展获得 token,该token在 MonoBehaviour 销毁时会自动调用取消
await UniTask.DelayFrame(1000, cancellationToken: this.GetCancellationTokenOnDestroy());
-
取消的内部实现是抛出 OperationCanceledException,因此外部可以通过捕获该异常来处理取消逻辑
参考下面的异常处理
超时处理
-
正常处理方式
var cts = new CancellationTokenSource(); // unitask扩展了 CancelAfterSlim,因此unity中建议用 CancelAfterSlim,标准c#中用 CancelAfter // 使用 CancelAfterSlim 必须保存返回值,并在异步结束调用 dispose 来停止定时器 IDisposable timer = cts.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 一般外部会传入一个 cancelToken,把2个token组合起来 // 其实我们可以直接用 cancelToken.CancelAfterSlim(1000) ,这样就不需要串联token, // 但是有2个问题,所以放弃该方案 // 一是无法区分是超时还是取消, // 二是超时取消后,cancelToken.IsCancellationRequested 将返回true,你别的地方不能用这个来判断是否已取消 // 注意不能直接用 cancelToken.CancelAfter,因为CancelAfter无法停止定时器 var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, cts.Token); try { await UnityWebRequest.Get("http://foo").SendWebRequest().WithCancellation(linkedTokenSource.Token); } catch (OperationCanceledException ex) { if (cancelToken.IsCancellationRequested) { UnityEngine.Debug.Log("Cancel."); } else { UnityEngine.Debug.Log("Timeout"); } } finally { timer.dispose(); // 销毁定时器 linkedTokenSource.Dispose(); cts.Dispose(); }
-
unitask提供的简便处理方式(不建议用这个,不方便跟其它 cancelToken 串联,可以用自己写的 TimeoutTokenSource)
-
CancellationTokenSource 的设计初衷是不能重复使用,也就是只要调用过Cancel就表示结束,要用再创建
而 unitask 扩展的 CancelAfterSlim 是设计为可重复使用的,为解决这个矛盾创建了 TimeoutController -
使用 TimeoutController 的好处
可以反复使用,不用重新创建
只需要的销毁的地方调用 timeoutController.Dispose() 就会触发取消,不用组合另一个 CancellationTokenSource -
缺点
不好跟其它 Token 串联,因为构造函数需要的是 TokenSource,其实内部也只用到Token,不知道为什么要传入 TokenSource
TimeoutController timeoutController = new TimeoutController(); CancellationTokenSource clickCancelSource = new CancellationTokenSource(); //跟别的取消组合,注意这里需要 CancellationTokenSource ,而不是 CancellationToken TimeoutController timeoutController = new TimeoutController(clickCancelSource); async UniTask FooAsync() { try { // timeoutController 可以反复调用 Timeout await UnityWebRequest.Get("http://foo").SendWebRequest() .WithCancellation(timeoutController.Timeout(TimeSpan.FromSeconds(5))); } catch (OperationCanceledException ex) { if (timeoutController.IsTimeout()) { UnityEngine.Debug.Log("timeout"); } } finally { timeoutController.Reset(); // 清除定时器 } } void OnDestroy() { timeoutController.Dispose(); // 会触发 Cancel }
-
异常处理
-
你在 async 函数中的抛出的异常如果没被捕获的话都会往上传递,
-
使用 UniTaskCompletionSource 的异步任务会捕获没有处理的异常,可以参考 UniTaskCompletionSource.TrySetResult 函数内部实现,
会调用 UniTaskScheduler.PublishUnobservedTaskException 函数,该函数会使用 UniTaskScheduler.UnobservedTaskException 函数来处理异常,
你也可以设置自己的处理函数,默认是写到日志中,日志级别可以通过 UniTaskScheduler.UnobservedExceptionWriteLogType 设置 -
使用 UniTaskCompletionSourceCore 的异步任务不会捕获没有处理的异常,可以参考 UniTaskCompletionSourceCore.TrySetResult(TResult result)
常见的 UniTask.Delay UniTask.WaitUntil 等都是使用 UniTaskCompletionSourceCore 实现,
为了捕获异常,应该在最外层的 UniTask 或 UniTaskVoid 上调用 Forget() 函数,这个函数也会捕获没有处理的异常,
具体参考 UniTask和UniTaskVoid区别
如果你最外层使用的是 async void 或 async Task ,则异常不会被捕获
-
-
有1个特殊的异常 OperationCanceledException ,你可以在 async 函数中抛出这个异常来表示取消操作,自定义任务一般是调用 utcs.TrySetCanceled();
上一层如果不想对取消做处理则一般要忽略该异常try { var x = await FooAsync(); return x * 2; } catch (Exception ex) when (!(ex is OperationCanceledException)) // when (ex is not OperationCanceledException) at C# 9.0 { return -1; }
PublishUnobservedTaskException 对于 OperationCanceledException 异常默认是不做任何处理,你可以设置 UniTaskScheduler.PropagateOperationCanceledException=true
来把 OperationCanceledException 当成普通异常处理 -
你也可以通过 SuppressCancellationThrow 把 OperationCanceledException 异常转成返回值
var (isCanceled, _) = await UniTask.DelayFrame(10, cancellationToken: cts.Token).SuppressCancellationThrow(); if (isCanceled) { // ... }
async/await的实现原理
-
参考 https://github.com/dotnet/roslyn/blob/main/docs/features/task-types.md
-
首先说一下 await 的本质
当我们执行 await expr; 时,本质上是执行下面的伪代码function await(expr) { var awaiter = expr.GetAwaiter(); // expr 必须有 GetAwaiter 函数 if ( awaiter is ICriticalNotifyCompletion awaiter1 ) { if ( !awaiter1.IsCompleted ) { awaiter.OnCompleted(action); // awaiter 必须有 OnCompleted 方法,接受一个action } } else if ( awaiter is INotifyCompletion awaiter2) { if ( !awaiter2.IsCompleted ) { awaiter.OnCompleted(action); // awaiter 必须有 OnCompleted 方法,接受一个action } } // 跟 typescript 的 await 原理是一样的,ts 的await相当于 promise.then(xxx) // 这里的 await task 相当于 task.GetAwaiter().onCompleted(xxx) }
-
示例
class MyDelay : INotifyCompletion { private readonly double _start; private readonly int _ms; public MyDelay(int ms) { _start = Util.ElapsedTime.TotalMilliseconds; _ms = ms; } internal MyDelay GetAwaiter() => this; public void OnCompleted(Action continuation) { Tick += Check; // 注册一个计时回调 void Check() { if (Util.ElapsedTime.TotalMilliseconds - _start > _ms) { continuation(); Tick -= Check; } } } public void GetResult() {} public bool IsCompleted => false; } // 则你可以在别的函数中 await new MyDelay(1000); 来延时等待
-
搞明白 await 后才能理解 async
async 其实是个语法糖,把函数中的代码变成一个状态机的执行过程,遇到 await 时把状态机的 moveNext 函数传给 OnCompleted
注意 await 只能在 async 中使用,这点跟 ts 的 async 很像,ts的 async 把函数转成了1个 promise.then 链
这里你也可以类似的理解, async 的函数转成了 task.GetAwaiter().onCompleted(xxx) 链
你如果要对状态机进行干预的话,需要创建你自己的 Task 对象[AsyncMethodBuilder(typeof (ETAsyncTaskMethodBuilder<>))] public class ETTask<T>: ICriticalNotifyCompletion { private static readonly Queue<ETTask<T>> queue = new Queue<ETTask<T>>(); public static ETTask<T> Create() { return new ETTask<T>(); } private AwaiterStatus state; private T value; private object callback; // Action or ExceptionDispatchInfo private ETTask() { } // 有这个才能被 await public ETTask<T> GetAwaiter() { return this; } // 状态机会调用获得返回值 public T GetResult() { switch (this.state) { case AwaiterStatus.Succeeded: return this.value; case AwaiterStatus.Faulted: ExceptionDispatchInfo c = this.callback as ExceptionDispatchInfo; this.callback = null; c?.Throw(); return default; default: throw new NotSupportedException("ETask does not allow call GetResult directly when task not completed. Please use 'await'."); } } public bool IsCompleted { get { return state != AwaiterStatus.Pending; } } // 状态机会调用 public void UnsafeOnCompleted(Action action) { if (this.state != AwaiterStatus.Pending) { action?.Invoke(); return; } this.callback = action; } public void OnCompleted(Action action) { this.UnsafeOnCompleted(action); } public void SetResult(T result) { if (this.state != AwaiterStatus.Pending) { throw new InvalidOperationException("TaskT_TransitionToFinal_AlreadyCompleted"); } this.state = AwaiterStatus.Succeeded; this.value = result; Action c = this.callback as Action; this.callback = null; c?.Invoke(); } public void SetException(Exception e) { if (this.state != AwaiterStatus.Pending) { throw new InvalidOperationException("TaskT_TransitionToFinal_AlreadyCompleted"); } this.state = AwaiterStatus.Faulted; Action c = this.callback as Action; this.callback = ExceptionDispatchInfo.Capture(e); c?.Invoke(); } }
这个 ETTask 就类似上面的 MyDelay (此时不需要上面的 AsyncMethodBuilder 特性),是一个可以被 await 的对象
{ ETTask<int> task = new ETTask<int> (); setTimerOut(2000, ()=>task.SetResult(10)); await task; // 这里会进入等待,因为 task.OnCompleted 啥都没做,只是设置回调 // 等 setTimerOut 回调后会调用 task.SetResult ,这会触发调用回调,使 Test 函数继续执行 }
我们可以看到,上面的 setTimerOut 是用一种回调的方式来继续任务的执行,为了更简便的书写代码,这才有了 async
async 只是一个语法糖,会把函数代码转成一个状态机来执行,比如下面的函数async ETTask<int> TestFunc() { int ret = 0; dosomething(); return ret; } // 将被转换成 ETTask<int> TestFunc() { XXXStateMachine stateMachine; // 当调用 TestFunc() 时,状态机已经开始运行 // 加上 await 只是为了在 TestFunc() 执行完成后继续执行后续代码 stateMachine.Start(); return stateMachine.GetTask(); }
这里你需要指定一个TaskBuilder来对状态机进行定制
public struct ETAsyncTaskMethodBuilder<T> { private ETTask<T> tcs; public static ETAsyncTaskMethodBuilder<T> Create() { ETAsyncTaskMethodBuilder<T> builder = new ETAsyncTaskMethodBuilder<T>() { tcs = ETTask<T>.Create() }; return builder; } public ETTask<T> Task => this.tcs; public void SetException(Exception exception) { this.tcs.SetException(exception); } public void SetResult(T ret) { this.tcs.SetResult(ret); } // 当 async 函数中遇到 await xxx; 时就会触发该函数,你可以做些额外的处理,但必须调用 awaiter.OnCompleted public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { awaiter.OnCompleted(stateMachine.MoveNext); } [SecuritySafeCritical] public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine { awaiter.OnCompleted(stateMachine.MoveNext); } // ETTask 执行入口 // stateMachine 是编译器把 async 函数中的代码编译成的状态机对象 public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { stateMachine.MoveNext(); } // stateMachine 是编译器把 async 函数中的代码编译成的状态机对象,用接口的方式会导至装箱操作,这样你可以根据需要把它保存起来 public void SetStateMachine(IAsyncStateMachine stateMachine) { } }
通过在 ETTask 类上加入 AsyncMethodBuilder 特性来让编译器知道,当async 函数返回 ETTask 时应该用哪个 Builder 来构建状态机
XXXStateMachine 是编译器自动生成的,类似下面这样XXXStateMachine { int state = 0; Start() { // 根据函数返回值 ETTask<int> 找到对应的Builder: ETAsyncTaskMethodBuilder<int>,并创建实例 ETAsyncTaskMethodBuilder<int> builder = ETAsyncTaskMethodBuilder<int>.Create(); builder.SetStateMachine(this); // 给 builder 一个缓存 stateMachine 的机会 builder.Start(this); // 开始执行状态机,在Start函数中你必须调用 stateMachine.MoveNext(); } MoveNext() { int ret; try { // 根据当前状态执行一段编译器自动生成的代码 switch(state) { case n: ...... Await(expr); break; } } catch { builder.SetException(e); // 出错 return; } builder.SetResult(ret); // 已经完成会在这里设置结果 } Await(expr) { if ( expr ) // 当函数中遇到 await expr; 时,编译成如下代码 { var awaiter = expr.GetAwaiter(); // expr 必须有 GetAwaiter 函数 if ( awaiter is ICriticalNotifyCompletion awaiter1 ) { if ( !awaiter1.IsCompleted ) { builder.AwaitUnsafeOnCompleted(awaiter, this); // builder.AwaitUnsafeOnCompleted 中必须调用 awaiter.OnCompleted(stateMachine.MoveNext); // awaiter.OnCompleted 中必须在完成等待后调用 action.Invoke() 来继续下一步 } } else if ( awaiter is INotifyCompletion awaiter2) { if ( !awaiter2.IsCompleted ) { builder.AwaitOnCompleted(awaiter, this); // builder.AwaitOnCompleted 中必须调用 awaiter.OnCompleted(stateMachine.MoveNext); // awaiter.OnCompleted 中必须在完成等待后调用 action.Invoke() 来继续下一步 } } } } GetTask() { return builder.Task; } }
可以看到在 builder.SetResult builder.SetException 最终都会调用 ETTask.callback 的回调,让代码继续执行
让我们总结下我们要把回调转成await的方式:- 设计一个通用任务类型,比如直接使用上面的 ETTask,有 GetAwaiter() 函数的才可以被 await,ETTaskVoid就不能被 await
- 设计通用任务类型对应的Builder,比如直接使用上面的 ETAsyncTaskMethodBuilder
- 设计一个返回task的函数,类似这样,编译器会生成状态机代码:
当我们调用 WaitAnimation(Animation anim) 时状态机就已经开始执行,await 只是关联后续代码ETTask<int> WaitAnimation(Animation anim) { ETTask<int> task = new ETTask<int>(); anim.setFinishedCallback(()=>task.SetResult(0)); return task; }
完善的代码实现请参考 unity 的 ET 框架中的这几个类:
ETTask ETAsyncTaskMethodBuilder TimerComponent.WaitTillAsyncETTask 用于无返回值,ETTask 用于有返回值
如果你不想定义自己的Task的话,可以用默认的 Task// 返回类型:只能返回 3 种类型 void、Task 和 Task<T> // 对于void方法必须加入try/catch捕获异常,其它2个会捕获该异常并将其置于 Task 对象上 // 参数:数量不限,但不能使用 out 和 ref 关键字 public static async Task<int> onButtonClick() { // 匿名方法和 Lambda 表达式也可以作为异步对象 var t = Task.Run(()=>{ Thread.Sleep(5000); return 100; }); // 包含 N(N>0) 个 await 表达式(不存在 await 表达式的话 IDE 会发出警告) return await t; }
-
特别注意
- async/await 本身是在一个线程上的,但 Task.Run 默认开辟了新线程来执行代码,
执行完后会使用执行任务前的 SynchronizationContext.Current 来执行后续代码
参考 Task多线程分析
你可以用 task.Wait() 在同一线程执行,但该线程会卡住 - 调用 async 函数时可以不用 await,此时 async 函数在第1个 await 卡住后,外部函数将继续执行,不会异步等待
- lambda 表达式如果用 async 修饰,默认返回 Task
- async/await 本身是在一个线程上的,但 Task.Run 默认开辟了新线程来执行代码,
-
还有一种循环任务 IAsyncEnumerator
async Task Main()
{
await foreach (var i in new F())
{
Console.Write(i + ", "); // 1, 2, 3, 4, 5,
}
}
class F
{
public async IAsyncEnumerator<int> GetAsyncEnumerator()
{
for (var i = 0; i < 5; ++i)
{
await Task.Delay(1);
yield return i;
}
}
}