async异步方案

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的函数,类似这样,编译器会生成状态机代码:
      	ETTask<int>	WaitAnimation(Animation anim)
      	{
      		ETTask<int> task = new ETTask<int>();
      		anim.setFinishedCallback(()=>task.SetResult(0));
      		return task;
      	}
      
      当我们调用 WaitAnimation(Animation anim) 时状态机就已经开始执行,await 只是关联后续代码

    完善的代码实现请参考 unity 的 ET 框架中的这几个类:
    ETTask ETAsyncTaskMethodBuilder TimerComponent.WaitTillAsync

    ETTask 用于无返回值,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
  • 还有一种循环任务 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;
			}
		}
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值