|
同步与异步程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用户层面来说,我们可以选择stop the world,等待操作完成返回结果后再继续操作,也可以选择继续去执行其他操作,等事件返回结果后再通知回来。这就是从用户角度来看的同步与异步。从操作系统的角度,同步异步,与任务调度,进程间切换,中断,系统调用之间有着更为复杂的关系。同步I/O 与 异步I/O的区别「为什么使用异步」用户可以阻塞式的等待,因为人的操作和计算机相比是非常慢的,计算机如果阻塞那就是很大的性能浪费了,异步操作让您的程序在等待另一个操作的同时完成工作。三种异步操作的场景:「I/O操作」:例如:发起一个网络请求,读写数据库、读写文件、打印文档等,一个同步的程序去执行这些操作,将导致程序的停止,直到操作完成。更有效的程序会改为在操作挂起时去执行其他操作,假设您有一个程序读取一些用户输入,进行一些计算,然后通过电子邮件发送结果。发送电子邮件时,您必须向网络发送一些数据,然后等待接收服务器响应。等待服务器响应所投入的时间是浪费的时间,如果程序继续计算,这将得到更好的利用「并行执行多个操作」:当您需要并行执行不同的操作时,例如进行数据库调用、Web 服务调用以及任何计算,那么我们可以使用异步「长时间运行的基于事件驱动的请求」:这就是您有一个请求进来的想法,并且该请求进入休眠状态一段时间等待其他一些事件的发生。当该事件发生时,您希望请求继续,然后向客户端发送响应。所以在这种情况下,当请求进来时,线程被分配给该请求,当请求进入睡眠状态时,线程被发送回线程池,当任务完成时,它生成事件并从线程池中选择一个线程发送响应计算机中异步的实现方式就是任务调度,也就是进程的切换「任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。」计算机系统分为用户空间和内核空间,用户进程在用户空间,操作系统运行在内核空间,内核空间的数据访问修改拥有高于普通进程的权限,用户进程之间相互独立,内存不共享,保证操作系统的运行安全。如何最大化的利用CPU,确定某一时刻哪个进程拥有CPU资源就是任务调度的过程。内核负责调度管理用户进程,以下为进程调度过程在任意时刻, 一个 CPU 核心上(processor)只可能运行一个进程每一个进程可以包含多个线程,线程是执行操作的最小单元,因此进程的切换落实到具体细节就是正在执行线程的切换FutureFuture 表示一个异步的操作结果,用来表示一个延迟的计算,返回一个结果或者error,使用代码实例:Futurefuture=getFuture();future.then((value)=>handleValue(value)).catchError((error)=>handleError(error)).whenComplete(func);future可以是三种状态:未完成的、返回结果值、返回异常当一个返回future对象被调用时,会发生两件事:「将函数操作入队列等待执行结果并返回一个未完成的Future对象」「函数操作完成时,Future对象变为完成并携带一个值或一个错误」首先,Flutter事件处理模型为先执行main函数,完成后检查执行微任务队列Microtask Queue中事件,最后执行事件队列Event Queue中的事件,示例:voidmain(){Future(()=>print(10));Future.microtask(()=>print(9));print("main");}///打印结果为:///main///9///10基于以上事件模型的基础上,看下Future提供的几种构造函数,其中最基本的为直接传入一个Function:factoryFuture(FutureOrcomputation()){_Futureresult=new_Future();Timer.run((){try{result._complete(computation());}catch(e,s){_completeWithErrorCallback(result,e,s);}});returnresult;}Function有多种写法://简单操作,单步Future(()=>print(5));//稍复杂,匿名函数Future((){print(6);});//更多操作,方法名Future(printSeven);printSeven(){print(7);}「Future.microtask」此工程方法创建的事件将发送到微任务队列Microtask Queue,具有相比事件队列Event Queue优先执行的特点factoryFuture.microtask(FutureOrcomputation()){_Futureresult=new_Future();//scheduleMicrotask((){try{result._complete(computation());}catch(e,s){_completeWithErrorCallback(result,e,s);}});returnresult;}「Future.sync」返回一个立即执行传入参数的Future,可理解为同步调用factoryFuture.sync(FutureOrcomputation()){try{varresult=computation();if(resultisFuture){returnresult;}else{//TODO(40014):Removecastwhentypepromotionworks.returnnew_Future.value(resultasdynamic);}}catch(error,stackTrace){///...}}Future.microtask(()=>print(9));Future(()=>print(10));Future.sync(()=>print(11));///打印结果:11、9、10「Future.value」创建一个将来包含value的futurefactoryFuture.value([FutureOrvalue]){returnnew_Future.immediate(value==nullvalueasT:value);}参数「FutureOr」含义为T value 和 Futurevalue 的合集,因为对于一个Future参数来说,他的结果可能为value或者是Future,所以对于以下两种写法均合法:Future.value(12).then((value)=>print(value));Future.value(Future((){return13;}));这里需要注意即使value接收的是12,仍然会将事件发送到Event队列等待执行,但是相对其他Future事件执行顺序会提前「Future.error」创建一个执行结果为error的futurefactoryFuture.error(Objecterror,[StackTracestackTrace]){///...returnnew_Future.immediateError(error,stackTrace);}_Future.immediateError(varerror,StackTracestackTrace):_zone=Zone._current{_asyncCompleteError(error,stackTrace);}Future.error(newException("errmsg")).then((value)=>print("errvaluevalue")).catchError((e)=>print(e));///执行结果为:Exception: err msg「Future.delayed」创建一个延迟执行回调的future,内部实现为Timer加延时执行一个FuturefactoryFuture.delayed(Durationduration,[FutureOrcomputation()]){///...newTimer(duration,(){if(computation==null){result._complete(nullasT);}else{try{result._complete(computation());}catch(e,s){_completeWithErrorCallback(result,e,s);}}});returnresult;}「Future.wait」等待多个Future并收集返回结果staticFuture>wait(Iterable>futures,{booleagerError=false,voidcleanUp(TsuccessValue)}){///...}FutureBuilder结合使用:child:FutureBuilder(future:Future.wait([firstFuture(),secondFuture()]),buildercontext,snapshot){if(!snapshot.hasData){returnCircularProgressIndicator();}finalfirst=snapshot.data[0];finalsecond=snapshot.data[1];returnText("data$first$second");},),「Future.any」返回futures集合中第一个返回结果的值staticFutureany(Iterable>futures){varcompleter=newCompleter.sync();voidonValue(Tvalue){if(!completer.isCompleted)completer.complete(value);}voidonError(Objecterror,StackTracestack){if(!completer.isCompleted)completer.completeError(error,stack);}for(varfutureinfutures){future.then(onValue,onErrornError);}returncompleter.future;}对上述例子来说,Future.any snapshot.data 将返回firstFuture 、secondFuture中第一个返回结果的值「Future.forEach」为传入的每一个元素,顺序执行一个actionstatic Future forEach(Iterable elements, FutureOr action(T element)) { var iterator = elements.iterator; return doWhile(() { if (!iterator.moveNext()) return false; var result = action(iterator.current); if (result is Future) return result.then(_kTrue); return true; }); }这里边action是方法作为参数,头一次见这种形式语法还是在js中,当时就迷惑了很大一会儿,使用示例:Future.forEach(["one","two","three"],(element){print(element);});「Future.doWhile」执行一个操作直到返回falseFuture.doWhile((){for(vari=0;i$i");if(i>=3){returnfalse;}}returntrue;});///结果打印到3以上为Future中常用构造函数和方法「在Widget中使用Future」Flutter提供了配合Future显示的组件FutureBuilder,使用也很简单,伪代码如下:child:FutureBuilder(future:getFuture(),buildercontext,snapshot){if(!snapshot.hasData){returnCircularProgressIndicator();}elseif(snapshot.hasError){return_ErrorWidget("Error{snapshot.error}");}else{return_ContentWidget("Result{snapshot.data}")}})Async-await使用这两个关键字提供了异步方法的「同步书写方式」,Future提供了方便的链式调用使用方式,但是不太直观,而且大量的回调嵌套造成可阅读性差。因此,现在很多语言都引入了await-async语法,学习他们的使用方式是很有必要的。两条基本原则:「定义一个异步方法,必须在方法体前声明 async」「await关键字必须在async方法中使用」首先,在要执行耗时操作的方法体前增加async:voidmain()async{···}然后,根据方法的返回类型添加Future修饰Futuremain()async{···}现在就可以使用await关键字来等待这个future执行完毕print(awaitcreateOrderMessage());例如实现一个由一级分类获取二级分类,二级分类获取详情的需求,使用链式调用的代码如下:varlist=getCategoryList();list.then((value)=>value[0].getCategorySubList(value[0].id)).then((subCategoryList){varcourseList=subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);print(courseList);}).catchError((e)=>(){print(e);});现在来看下使用async/await,事情变得简单了多少Futuremain()async{awaitgetCourses().catchError((e){print(e);});}FuturegetCourses()async{varlist=awaitgetCategoryList();varsubCategoryList=awaitlist[0].getCategorySubList(list[0].id);varcourseList=subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);print(courseList);}可以看到这样更加直观缺陷async/await 非常方便,但是还是有一些「缺点」需要注意因为它的代码看起来是同步的,所以是「会阻塞后面的代码执行」,直到await返回结果,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但后边自己的代码被阻塞。这意味着代码可能会由于有大量await代码相继执行而阻塞,本来用Future编写表示并行的操作,现在使用await变成了串行,例如,首页有一个同时获取轮播接口,tab列表接口,msg列表接口的需求FuturegetBannerList()async{returnawaitFuture.delayed(Duration(seconds:3),(){return"bannerlist";});}FuturegetHomeTabList()async{returnawaitFuture.delayed(Duration(seconds:3),(){return"tablist";});}FuturegetHomeMsgList()async{returnawaitFuture.delayed(Duration(seconds:3),(){return"msglist";});}使用await编写很可能会写成这样,打印执行操作的时间Futuremain2()async{varstartTime=DateTime.now().second;awaitgetBannerList();awaitgetHomeTabList();awaitgetHomeMsgList();varendTime=DateTime.now().second;print(endTime-startTime);//9}在这里,我们直接等待所有三个模拟接口的调用,使每个调用3s。后续的每一个都被迫等到上一个完成, 最后会看到总运行时间为9s,而实际我们想三个请求同时执行,代码可以改成如下这种:Futuremain()async{varstartTime=DateTime.now().second;varbannerList=getBannerList();varhomeTabList=getHomeTabList();varhomeMsgList=getHomeMsgList();awaitbannerList;awaithomeTabList;awaithomeMsgList;varendTime=DateTime.now().second;print(endTime-startTime);//3}将三个Future存储在变量中,这样可以同时启动,最后打印时间仅为3s,所以在编写代码时,我们必须牢记这点,避免性能损耗。原理线程模型当一个Flutter应用或者Flutter Engine启动时,它会启动(或者从池中选择)另外三个线程,这些线程有些时候会有重合的工作点,但是通常,它们被称为UI线程,GPU线程,IO线程。需要注意一点这个UI线程并不是程序运行的主线程,或者说和其他平台上的主线程理解不同,通常的,Flutter将平台的主线程叫做"Platform thread"imgUI线程是所有的Dard代码运行的地方,例如framework和你的应用,除非你启动自己的isolates,否则Dart将永远不会运行在其他线程。平台线程是所有依赖插件的代码运行的地方。该线程也是native frameworks为其他任务提供服务的地方,一般来说,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个Platform thread为其提供服务。跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread,试图在其它线程中调用Flutter Engine会导致无法预期的异常。这跟Android/iOS UI相关的操作都必须在主线程进行相类似。「Isolates」是Dart中概念,本意是隔离,它的实现功能和thread类似,但是他们之间的实现又有着本质的区别,「Isolote」是独立的工作者,它们之间不共享内存,而是通过channel传递消息。Dart是单线程执行代码,Isolate提供了Dart应用可以更好的利用多核硬件的解决方案。事件循环单线程模型中主要就是在维护着一个事件循环(Event Loop) 与 两个队列(event queue和microtask queue)当Flutter项目程序触发如点击事件、IO事件、网络事件时,它们就会被加入到eventLoop中,eventLoop一直在循环之中,当主线程发现事件队列不为空时发现,就会取出事件,并且执行。microtask queue中事件优先于event queue执行,当有任务发送到microtask队列时,会在当前event执行完成后,阻塞当前event queue转而去执行microtask queue中的事件,这样为Dart提供了任务插队的解决方案。「event queue的阻塞意味着app无法进行UI绘制,响应鼠标和I/O等事件」,所以要谨慎使用,如下为流程图:event queue和microtask queue这两个任务队列中的任务切换在某些方面就相当于是「协程调度机制」协程协程是一种「协作式」的任务调度机制,区别于操作系统的抢占式任务调度机制,它是「用户态」下面的,避免线程切换的内核态、用户态转换的性能开销。它让调用者自己来决定什么时候让出cpu,比操作系统的抢占式调度所需要的时间代价要小很多,后者为了恢复现场会保存相当多的状态(不仅包括进程上下文的虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态),并且会频繁的切换,以现在流行的大多数Linux机器来说,每一次的上下文切换要消耗大约1.2-1.5μs的时间,这是仅考虑直接成本,固定在单个核心以避免迁移的成本,未固定情况下,切换时间可达2.2μs对cpu来说这算一个很长的时间吗,一个很好的比较是memcpy,在相同的机器上,完成一个64KiB数据的拷贝需要3μs的时间,上下文的切换比这个操作稍微快一些协程和线程非常相似,是从异步执行任务的角度来看,而并不是从设计的实体角度像进程->线程->协程这样类似于细胞->原子核->质子中子这样的关系。可以理解为线程上执行的一段函数,用「yield」完成异步请求、注册回调/通知器、保存状态,挂起控制流、收到回调/通知、恢复状态、恢复控制流的所有过程多线程执行任务模型如图:线程的阻塞要靠系统间进程的切换,完成逻辑流的执行,频繁的切换耗费大量资源,而且逻辑流的执行数量严重依赖于程序申请到的线程的数量。协程是协同多任务的,这意味着「协程提供并发性但不提供并行性」,执行流模型图如下:协程可以用逻辑流的顺序去写控制流,协程的等待会主动释放cpu,避免了线程切换之间的等待时间,有更好的性能,逻辑流的代码编写和理解上也简单的很多但是线程并不是一无是处,「抢占式线程调度器事实上提供了准实时的体验」。例如Timer,虽然不能确保在时间到达的时候一定能够分到时间片运行,但不会像协程一样万一没有人让出时间片就永远得不到运行……总结同步与异步Future提供了Flutter中异步代码链式编写方式async-wait提供了异步代码的同步书写方式Future的常用方法和FutureBuilder编写UIFlutter中线程模型,四个线程单线程语言的事件驱动模型进程间切换和协程对比参考https://dart.cn/tutorials/language/futureshttps://dart.cn/codelabs/async-awaithttps://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a#event-queue-new-futurehttps://juejin.cn/post/6844903620064837645https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_awaithttps://www.zhihu.com/question/19732473https://www.zhihu.com/question/50185085/answer/183463734https://en.wikipedia.org/wiki/Asynchrony_(computer_programming)https://eli.thegreenplace.net/2018/measuring-context-switching-and-memory-overheads-for-linux-threads/
|
|