异步处理&同步处理
同步处理:简单说就是代码按顺序执行,在方法1里调用方法2时,要等到方法2执行完毕才接着执行方法1的代码。
异步处理:简单说就是在两个方法里的代码同时或者来回执行,在方法1里调用方法2时,不等方法2执行完就接着执行接下来的代码。
异步不等于多线程
异步处理不等于多线程,因为即使是单线程,也可以通过切换执行的代码来实现异步。典型的例子就是unity的协程。协程就是只运行在主线程来实现异步处理的。
而C#里真正跟多线程相关的是把ThreadPool封装后的Task类。Task类通常通过async/await 来实现异步,但异步和多线程是两个不同的概念。
async/await 和 Task
这两个关键字是C#5.0引进的,本质是由编译器提供的语法糖,来方便进行异步编程用的。对于unity开发者来说,可以看成一个升级版的协程。
//协程版等待一秒
IEnumerator DelayCoroutine()
{
Debug.Log("Start");
yield return new WaitForSeconds(1f);
Debug.Log("End");
}
//Async版等待一秒
async void DelayTask()
{
Debug.Log("Start");
await Task.Delay(1000);
Debug.Log("End");
}
async/await 与 Coroutine 相比的优点
- 由于是C#提供的功能,所以在非Mono脚本里也能实现异步。
- 可以方便的拿到异步的返回值。
//异步方法,会在最后返回一个string
async Task<string> DelayTask()
{
Debug.Log("Start");
await Task.Delay(1000);
Debug.Log("End");
return "Completed";
}
由于async可以在任何方法前加,同理适用于unity的生命周期函数。
async void Start()
{
var task = DelayTask();
Debug.Log("异步执行中..");
var str = await task;//等待异步结果
Debug.Log(str);
}
- 避免回调地狱
有的时候我们希望在执行完异步操作时执行一个回调方法,但如果这个回调也有异步操作也要回调,就会造成回调的嵌套,降低代码的可读性。
协程的话可以将各个回调做成一个个小协程,之后在一个主协程里yield return。但是由于协程无法返回值,导致如果想要用上一个协程计算出的值的话,只能将回调作为委托传进去,无法避免回调的嵌套。
但async/await是可以返回值的,可以把回调改写成await的顺序执行。
async void Start()
{
var task = DelayTask();
Debug.Log($"异步执行中..");
var str = await task;//等待异步结果
var task2 = AsyncFun2(str);//利用第一个结果执行第二个异步方法
Debug.Log($"异步执行中..");
str = await task2();//等待第二个异步结果
Debug.Log(str);
}
4.async/await是可以用Try-Catch捕获异常,协程不行。
task 取消的问题
async/await需要明确地取消正在执行的异步方法,比较麻烦。
由于async/await异步实现是依靠着Task实例。Task实例是有可能是多线程的,由于线程是操作系统层面的资源就导致无法直接停止一个Task。所以我们只能做一个公共变量,task在执行异步时不断检查这个变量是否改变,改变的话说明要停止执行,在Task内部自己停止。
C#提供一个“取消标记”叫做CancellationTokenSource.Token,在创建task的时候传入此参数,就可以将主线程和任务相关联,然后在任务中设置“取消信号“叫做ThrowIfCancellationRequested来等待主线程使用Cancel来通知,一旦cancel被调用。task将会抛出OperationCanceledException来中断此任务的执行,最后将当前task的Status的IsCanceled属性设为true。
注意:一定要处理这个异常,可以通过调用Task.Result成员来获取这个异常。如果一直不查询Task的Exception属性。你的代码就永远注意不到这个异常的发生,如果不能捕捉到这个异常,垃圾回收时,抛出AggregateException,进程就会立即终止,这就是“牵一发动全身”,莫名其妙程序就自己关掉了,谁也不知道这是什么情况。所以,必须调用前面提到的某个成员,确保代码注意到异常,并从异常中恢复。因此可以将调用Task的某个成员来检查Task是否跑出了异常,通常调用Task的Result。
而协程只要把调用这个协程的GameObject删了就会停止协程。或者在开启协程时记下协程实例,要取消时调用StopCoroutine(coroutine)就行。主要原因就是await可以返回值,如果中途取消,就可能导致后面的代码异常,所以只能抛异常。
UniTask
虽然在Unity(2017版本以上)中可以正常地使用async/await和Task类,但是C#自带地Task类过于繁重而且一些unity里常用的功能要自己实现和封装。于是CySharp公司推出了UniTask来解决这个痛点。
用UniTask有以下优点:
- 用法和和原先的Task类用法一致。(Task-Like)
- 比Task更轻量,占用内存少。
- 对async/await 的优化,实现大幅减少GC。
- 提供unity相关的功能。
- 提供各种Awaiter。
- 实现在editor下await状态的可视化。(利用UniTaskTracker)
但对Unity版本有要求,需要使用Unity2018.3以上版本。
对同一个UniTask实例不能两次await,不然会报错。
生成UniTask实例的方法
-
利用async/await 同C#的用法一样,只不过是将返回值改成相应的UniTask的结构体。
Task ——> UniTask
Task<T> ——> UniTask<T>
void ——> UniTaskVoid //用于不需要返回UniTask的异步方法 -
利用UniTaskCompletionSource创建
用法如下:
async void Start()
{
var source = new UniTaskCompletionSource();
ReadyForCompleted(source).Forget();//只引发不考虑其是否完成
Debug.Log("Do Something...");
source.TrySetResult();//设置完成
//source.TrySetException(Exception);//设置失败
//source.TrySetCanceled();//设置取消
Debug.Log("Completed");
}
async UniTask ReadyForCompleted(