异步处理&同步处理

同步处理:简单说就是代码按顺序执行,在方法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 相比的优点

  1. 由于是C#提供的功能,所以在非Mono脚本里也能实现异步。
  2. 可以方便的拿到异步的返回值。
//异步方法,会在最后返回一个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);}
  1. 避免回调地狱
    有的时候我们希望在执行完异步操作时执行一个回调方法,但如果这个回调也有异步操作也要回调,就会造成回调的嵌套,降低代码的可读性。
    协程的话可以将各个回调做成一个个小协程,之后在一个主协程里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有以下优点:

  1. 用法和和原先的Task类用法一致。(Task-Like)
  2. 比Task更轻量,占用内存少。
  3. 对async/await 的优化,实现大幅减少GC。
  4. 提供unity相关的功能。
  5. 提供各种Awaiter。
  6. 实现在editor下await状态的可视化。(利用UniTaskTracker)

但对Unity版本有要求,需要使用Unity2018.3以上版本。
对同一个UniTask实例不能两次await,不然会报错。

生成UniTask实例的方法

  1. 利用async/await 同C#的用法一样,只不过是将返回值改成相应的UniTask的结构体。
    Task ——> UniTask
    Task<T> ——> UniTask<T>
    void ——> UniTaskVoid //用于不需要返回UniTask的异步方法

  2. 利用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(UniTaskCompletionSource source){Debug.Log("等待");await source.Task;Debug.Log("完成");}

其实就是起一个Task,可以手动的设置是否完成,异常或者取消。
相应的有一个泛型类UniTaskCompletionSource<T>,可以设置返回值。
注意一旦执行了TrySet其中一个,则该实例再执行其他TrySet方法是无效果的。
注意:这个生成的UniTask是可以重复await的。

  1. AutoResetUniTaskCompletionSource.Create()
    2.0版本加入的一个UniTaskCompletionSource的池化版本,用法同UniTaskCompletionSource,只是获取实例的方法不同。而且这个只能await一次,因为要被回收走。适合在局部作用域里使用,随用随扔,

UniTask常用的静态方法

  1. UniTask.Run(Action)/ UniTask.Run(Function);
    用法同对于的Task.Run方法,就是将委托内容方法放在线程池里运行。运行完毕后返回主线程(configawait设为true时)。
  2. UniTask.Delay
    /返回一个延迟几秒完成的UniTask,能选择是以什么update时间来计算。
UniTask.Delay(1000); //延迟1000ms
UniTask.Delay(TimeSpan.FromSeconds(1));//延迟1s
UniTask.Delay(1000, delayTiming: PlayerLoopTiming.FixedUpdate);//以FixedUpdate的时间来等待
  1. UniTask.DelayFrame //返回一个延迟几帧后完成的UniTask
UniTask.DelayFrame(3);//等待3帧(默认 update循环)
UniTask.DelayFrame(3, PlayerLoopTiming.FixedUpdate);//等待3帧(Fixedupdate循环)
  1. UniTask.Yield() //等待1帧
    可以用于将处理调回主线程用。例如yield之前是在其他线程跑,yield之后回到主线程跑。
    默认是update循环。通过变更loop的类型,能切换之后代码的运行时机。
 await UniTask.Yield();//等待update()下的一帧Debug.Log(Time.time);await UniTask.Yield(PlayerLoopTiming.FixedUpdate);//等待下一次fixedUpdate//↓↓↓从这之后都是以fixupdate的时机来执行↓↓↓Debug.Log(Time.time);await UniTask.Yield();//等待一帧,回到update的时机来执行//↓↓↓从这之后都是以update的时机来执行↓↓↓Debug.Log(Time.time);
  1. UniTask.SwitchToThreadPool / UniTask.SwitchToMainThread
    用来切换代码是在主线程跑还是线程池里跑。
await UniTask.Yield();
//之后都在主线程跑
await UniTask.SwitchToThreadPool();
//之后都在线程池跑
await UniTask.SwitchToMainThread();
//之后回到主线程跑

yield和SwitchToMainThread区别在于,如果已经是主线程下的话,SwitchToMainThread不会再等待一帧,而yield无论是不是在主线程,都会等待1帧。

  1. UniTask.WaitUntil/UniTask.WaitWhile
    类似与协程里用的WaitUntil和WaitWhile,可以指定是哪一个循环里Check。
await UniTask.WaitUntil(()=> isActiveAndEnabled,PlayerLoopTiming.FixedUpdate);
  1. UniTask.WaitUntilValueChanged
    等到指定对象的参数发生变化时,才完成。
var str = await UniTask.WaitUntilValueChanged(this.transform,x =>x.position);//第一个参数时判断目标,第二个参数是判断方法的委托。如果这个返回值变的话,即为发生变化。
Debug.Log(str);

注意:检测的target是一个弱引用,即可能会被GC回收。如果被GC回收的话,await就会被取消。

  1. UniTask.WhenAll(List)
    同Task.WhenAll()等待所有Task完成后完成,但UniTask版可以返回不同类型的值。
var num = UniTask.Run(()=>1);
var fl = UniTask.Run(()=>0.5f);
var str = UniTask.Run(()=>"aa");
var (p1, p2, p3) = await UniTask.WhenAll(num, fl, str);
  1. UniTask.WhenAny(List)
    同Task.WhenAny()等待其中一个Task完成即为完成。
 /// <summary>/// 返回最先ping的IPAddress/// </summary>/// <param name="apiHost"></param>/// <returns></returns>private async UniTask<IPAddress> SelectHostAsync(IPAddress[] apiHost){var tasks = apiHost.Select(PingAsync).ToArray();//不考虑取消var (_, result) = await UniTask.WhenAny(tasks);return result;}private async UniTask<IPAddress> PingAsync(IPAddress iP){var ping = new Ping(iP.ToString());while (!ping.isDone){await UniTask.Yield();}return iP;}

以下是2.0后加的方法
12. UniTask.Create<T>(Function(UniTask<T>))
用异步委托快速生成返回UniTask的异步方法。

        UniTask.Create(async ()=> {Debug.Log("aa");await UniTask.Delay(1000);return "11"; });
  1. UniTask.Defer(Function(UniTask<T>))
    用异步委托快速生成返回UniTask的异步方法,但在创建时不执行,但在await时才执行。
UniTask.Defer(async () => {Debug.Log("aa");await UniTask.Delay(1000);return "11";});
  1. UniTask.Lazy(Function(UniTask<T>))
    用异步委托生成一个AsyncLazy型对象,在创建时不执行,但在await时才执行。与Defer不同的是这个可以重复await。
var asyncLazy = UniTask.Lazy(async () =>{Debug.Log("aa");await UniTask.Delay(1000);return "11";});await asyncLazy.Task;
  1. UniTask.Void(Function(UniTask<T>))
    直接启动一个异步委托,不考虑其等待。
 UniTask.Void(async () => {Debug.Log("aa");await UniTask.Delay(1000);});
  1. UniTask.Action/UnityAction(Function(UniTask<T>))
    就是将异步委托封装成Action或UnityAction。
UniTask.Action(async () => {Debug.Log("aa");await UniTask.Delay(1000);});

等同于:

()=>
{UniTask.Void(async () => {Debug.Log("aa");await UniTask.Delay(1000);});
};
  1. uniTask.Timeout/TimeoutWithoutException()
    UniTask的实例可以调用Timeout/TimeoutWithoutException()方法来控制超时。两个方法不同点在于抛不抛异常。
     //1秒内无法的话直接抛异常var str = await DelayTask(token).Timeout(TimeSpan.FromSeconds(1));//1秒内无法完成的话,await本身完成。//同时complete = falsevar (complete, result) = await          DelayTask(token).TimeoutWithoutException(TimeSpan.FromSeconds(1));

Unity对象的扩展——Awaiter

对于一些需要用到等待的Unity对象提供GetAwaiter()功能,从而拿到Awaiter对象就可以进行await了。UniTask已经对各种各样的Unity对象进行了GetAwaiter的扩展。

  1. Coroutine的Awaiter
    可以直接对协程方法进行await 来调用和等待。
async void Start(){await DelayCoroutine();}
IEnumerator DelayCoroutine()
{Debug.Log("Start");yield return new WaitForSeconds(1f);Debug.Log("End");
}

相应的,UniTask实例也可以转化成Coroutine。

IEnumerator DelayCoroutine()
{Debug.Log("Start");yield return UniTask.Delay(1000).ToCoroutine();Debug.Log("End");
}
  1. AsyncOperation的Awaiter
    Unity本身自带的一些异步方法,也可以用await了。
    例如:
//AsyncOperation的wait
await SceneManager.LoadSceneAsync("NextScene");
//ResourceRequest的wait
await Resources.LoadAsync<Texture>("Icon").ToUniTask();
//AssetBundle加载的wait
await AssetBundle.LoadFromFileAsync("ABPath");
//UnityWebRequestAsyncOperation的wait
var urw = UnityWebRequest.Get("http://unity.com/");
await urw.SendWebRequest();

如果需要检查加载的进度的话,要创建一个Progree实例传进去。

var progress = Progress.Create<float>(f => Debug.Log($"进度是:{f}"));var urw = UnityWebRequest.Get("http://unity.com/");await urw.SendWebRequest().ToUniTask(progress: progress);
  1. UGUI的一些响应方法也可以await
 public Button btn;public Toggle tog;public InputField inputField;public Slider slider;async void Start(){//获取tokenvar token = this.GetCancellationTokenOnDestroy();//只想等待一次的话await btn.OnClickAsync();await tog.OnValueChangedAsync();await inputField.OnEndEditAsync();await slider.OnValueChangedAsync();//想等待多次的话//按键点击var btnEventHandler = btn.GetAsyncClickEventHandler(token);await btnEventHandler.OnClickAsync();//Toggle状态更新var togEventHandler = tog.GetAsyncValueChangedEventHandler(token);await togEventHandler.OnValueChangedAsync();//InputField输入完成var inputEventHandler = inputField.GetAsyncEndEditEventHandler(token);await inputEventHandler.OnEndEditAsync();//slider更新var sliderEventHandler = slider.GetAsyncValueChangedEventHandler(token);await sliderEventHandler.OnValueChangedAsync();}
  1. MonoBehaviour的回调函数也可以await
     //碰撞相关var collisionEnterTrigger = this.GetAsyncCollisionEnterTrigger();var collisionExitTrigger = this.GetAsyncCollisionExitTrigger();var collisionStayTrigger = this.GetAsyncCollisionStayTrigger();var enter = await collisionEnterTrigger.OnCollisionEnterAsync();var exit = await collisionExitTrigger.OnCollisionExitAsync();var stay = await collisionStayTrigger.OnCollisionStayAsync();//动画相关var animatorIKTrigger = this.GetAsyncAnimatorIKTrigger();var animatorMoveTrigger = this.GetAsyncAnimatorMoveTrigger();var layerIndex = await animatorIKTrigger.OnAnimatorIKAsync();await animatorMoveTrigger.OnAnimatorMoveAsync();//Visiblevar visibleTrigger = this.GetAsyncBecameVisibleTrigger();var InvisibleTrigger = this.GetAsyncBecameInvisibleTrigger();await visibleTrigger.OnBecameVisibleAsync();await InvisibleTrigger.OnBecameInvisibleAsync();
  1. DoTween也可以等待
    从OpenUPM导入DOTween后,添加“UNITASK_DOTWEEN_SUPPORT”宏后可以用。
await DoMove(...)
await(//同时执行两个Task,直到两个task都完成。DoMove(...).ToUniTask();DoMove(...).ToUniTask();
)

取消正在执行的异步的方法

  1. CancellationToken
    这个实例本身就是C#用来控制Task取消的类。创建方法如下:
        //生成Tokenvar tokenSource = new CancellationTokenSource();var token = tokenSource.Token;//将Token设成取消tokenSource.Cancel();//可以判断token是否取消了if (token.IsCancellationRequested){Debug.Log("Cancel");}token.ThrowIfCancellationRequested();//如果token是cancel的话,就抛出OperationCanceledException异常。

但每次都新生成一个Token很麻烦,有时候就是想在脚本被销毁时,把挂在它身上的异步方法给停下来。

var token2 = this.GetCancellationTokenOnDestroy();

一旦UniTask被Cancel的话,UniTask就会在一个Cancel状态。且如果是在await的话,await之后的代码都不会执行。尽量不要省略这个token,在能传的异步方法里把这个传进去。
在一些方法里没有办法传token时就要手动在代码里去判断。例如:

private async UniTask<string> ReadTxtAsync(string path, CancellationToken token){return await UniTask.Run(() => {//执行前确认token.ThrowIfCancellationRequested();var str = File.ReadAllText(path);//执行后确认token.ThrowIfCancellationRequested();return str;});}
  1. OperationCanceledException异常
    在UniTask里抛出这个异常的话,UniTask就会处于Cancal状态。同时UniTask会吃掉这个异常,不会打出errorlog。
    Cancel是一个外部操作,所以应该规定只有收到外部要求cancel时才能抛出这个异常。不应该程序内部自己判断来抛。同时如果在UniTask里try-catch时请把这个异常传出去,不要拦截。
private async UniTask TaskFunc(CancellationToken token)
{try{await UniTask.Delay(1000, cancellationToken : token);}catch (Exception e) when(!(e is OperationCanceledException)){Debug.LogError("Error");}
}

注意:这个异常只能用于Cancel时抛出,不应用于其他用途。
注意:在UniTask里抛出其他别的异常,UniTask就会变为失败

Editor下对UniTask的监控

Window/UniTask Tracker,可以查看现在运行中的UniTask,确认是否有泄露的UniTask。

UniTask使用笔记相关推荐

  1. 开源库UniTask笔记

    内容来源:up主游戏石匠,仅作笔记,推荐关注该up主. UniTask是Github上的开源库,为Unity提供一个高性能异步方案,可以代替协程实现异步操作,中文文档 优点: 不需要依赖于MonoBe ...

  2. 【读书笔记】知易行难,多实践

    前言: 其实,我不喜欢看书,只是喜欢找答案,想通过专业的解答来解决我生活的困惑.所以,我听了很多书,也看了很多书,但看完书,没有很多的实践,导致我并不很深入在很多时候. 分享读书笔记: <高效1 ...

  3. 【运维学习笔记】生命不息,搞事开始。。。

    001生命不息,搞事不止!!! 这段时间和hexesdesu搞了很多事情! 之前是机械硬盘和固态硬盘的测速,我就在那默默的看着他一个硬盘一个机械测来测去. 坐在他后面,每天都能看到这位萌萌的小男孩,各 ...

  4. SSAN 关系抽取 论文笔记

    20210621 https://zhuanlan.zhihu.com/p/353183322 [KG笔记]八.文档级(Document Level)关系抽取任务 共指id嵌入一样 但是实体嵌入的时候 ...

  5. pandas以前笔记

    # -*- coding: utf-8 -*- """ Created on Sat Jul 21 20:06:20 2018@author: heimi "& ...

  6. PyTorch 学习笔记(六):PyTorch hook 和关于 PyTorch backward 过程的理解 call

    您的位置 首页 PyTorch 学习笔记系列 PyTorch 学习笔记(六):PyTorch hook 和关于 PyTorch backward 过程的理解 发布: 2017年8月4日 7,195阅读 ...

  7. 容器云原生DevOps学习笔记——第三期:从零搭建CI/CD系统标准化交付流程

    暑期实习期间,所在的技术中台-效能研发团队规划设计并结合公司开源协同实现符合DevOps理念的研发工具平台,实现研发过程自动化.标准化: 实习期间对DevOps的理解一直懵懵懂懂,最近观看了阿里专家带 ...

  8. 容器云原生DevOps学习笔记——第二期:如何快速高质量的应用容器化迁移

    暑期实习期间,所在的技术中台-效能研发团队规划设计并结合公司开源协同实现符合DevOps理念的研发工具平台,实现研发过程自动化.标准化: 实习期间对DevOps的理解一直懵懵懂懂,最近观看了阿里专家带 ...

  9. 王道考研 计算机网络笔记 第六章:应用层

    本文基于2019 王道考研 计算机网络: 2019 王道考研 计算机网络 个人笔记总结 第一章:王道考研 计算机网络笔记 第一章:概述&计算机网络体系结构 第二章:王道考研 计算机网络笔记 第 ...

最新文章

  1. mysql学习资料_一不小心,我就上传了 279674 字的 MySQL 学习资料到 github 上了
  2. java类同步,Java同步工具類(一)
  3. 8086汇编与c++编译器就内存方面的感想
  4. windows离线安装grunt_chrome火狐离线安装包下载
  5. Python统计一个字符串中所有字符在另一个字符串出现的总次数
  6. 查询表中id相同的记录mysql_mysql – 从两个表中的ID相同的两个表中选择数据
  7. [CF364D]Ghd
  8. 有关java开发的单词_Java开发常用英语单词表
  9. OpenGL-坐标系统,进入3D世界(深度测试)
  10. 【史上最强】据说是气死了99名老师的作文
  11. struts1 使用poi组件 读取excel文件,创建excel ,输出excel文件
  12. html自动半角转全角,全角半角转换就是这么简单
  13. Ubuntu18.04 安装 ROS Melodic(同时解决 rosdep update 问题,亲测有效)
  14. Rocksdb Compaction 源码详解(一):SST文件详细格式源码解析
  15. 北京数码视讯s905l固件_数码视讯Q6联通版S905L芯片第三方刷机免拆卡刷固件
  16. NLP-预训练模型-2020:Electra【预训练任务RTD(ReplacedTokenDetection)替代MLM;借鉴GAN;生成器+判别器;判别器用于下游;比RoBert预训练速度大幅提升】
  17. 西门子SCL编程:SCL编写的DCS电机块
  18. [C++程序设计](入门级题解)级数求和
  19. 访问学者美国访学哪些东西不能带?
  20. 技术面试与 HR 谈薪资技巧

热门文章

  1. python 3.9 性能_Python 3.9 性能优化:更快的 list()、dict() 和 range() 等内置类型
  2. 浙大与北大计算机考研分数线,34所985大学考研分数线全部出炉,清华大学北大浙大等都接收调剂...
  3. js获取服务器响应头信息,请问,js中请求头信息和返回头信息的方法
  4. java partialfunction,scala中方法和函数的区别
  5. java中子类怎样调用父类的属性_java的继承、重载(overload)、覆盖(override)的总结...
  6. 大数据分块_空间数据库基础理论 GIS空间数据处理分析涉及的基本概念
  7. redhat怎样修改语言_硕士博士个人陈述(PS)辅导及修改服务带你极速前进!
  8. seo从入门到精通_新手学习SEO一个月能学会吗?
  9. 设计师如何了解行业方向?推荐设计师交流平台
  10. 疯狂电商购物节日,设计师受虐加班? | 美妆促销页面设计技巧