Flutter中的异步
同步与异步
程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用户层面来说,我们可以选择stop the world,等待操作完成返回结果后再继续操作,也可以选择继续去执行其他操作,等事件返回结果后再通知回来。这就是从用户角度来看的同步与异步。
从操作系统的角度,同步异步,与任务调度,进程间切换,中断,系统调用之间有着更为复杂的关系。
同步I/O 与 异步I/O的区别

为什么使用异步
用户可以阻塞式的等待,因为人的操作和计算机相比是非常慢的,计算机如果阻塞那就是很大的性能浪费了,异步操作让您的程序在等待另一个操作的同时完成工作。三种异步操作的场景:
- I/O操作:例如:发起一个网络请求,读写数据库、读写文件、打印文档等,一个同步的程序去执行这些操作,将导致程序的停止,直到操作完成。更有效的程序会改为在操作挂起时去执行其他操作,假设您有一个程序读取一些用户输入,进行一些计算,然后通过电子邮件发送结果。发送电子邮件时,您必须向网络发送一些数据,然后等待接收服务器响应。等待服务器响应所投入的时间是浪费的时间,如果程序继续计算,这将得到更好的利用
- 并行执行多个操作:当您需要并行执行不同的操作时,例如进行数据库调用、Web 服务调用以及任何计算,那么我们可以使用异步
- 长时间运行的基于事件驱动的请求:这就是您有一个请求进来的想法,并且该请求进入休眠状态一段时间等待其他一些事件的发生。当该事件发生时,您希望请求继续,然后向客户端发送响应。所以在这种情况下,当请求进来时,线程被分配给该请求,当请求进入睡眠状态时,线程被发送回线程池,当任务完成时,它生成事件并从线程池中选择一个线程发送响应
计算机中异步的实现方式就是任务调度,也就是进程的切换
任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。
计算机系统分为用户空间
和内核空间
,用户进程在用户空间,操作系统运行在内核空间,内核空间的数据访问修改拥有高于普通进程的权限,用户进程之间相互独立,内存不共享,保证操作系统的运行安全。如何最大化的利用CPU,确定某一时刻哪个进程拥有CPU资源就是任务调度的过程。内核负责调度管理用户进程,以下为进程调度过程

在任意时刻, 一个 CPU 核心上(processor)只可能运行一个进程
每一个进程可以包含多个线程,线程是执行操作的最小单元,因此进程的切换落实到具体细节就是正在执行线程的切换
Future
Future<T>表示一个异步的操作结果,用来表示一个延迟的计算,返回一个结果或者error
,使用代码实例:
1 | Future<int> future = getFuture(); |
future可以是三种状态:未完成的
、返回结果值
、返回异常
当一个返回future对象被调用时,会发生两件事:
- 将函数操作入队列等待执行结果并返回一个未完成的Future对象
- 函数操作完成时,
Future
对象变为完成并携带一个值或一个错误
首先,Flutter事件处理模型为先执行main函数,完成后检查执行微任务队列Microtask Queue
中事件,最后执行事件队列Event Queue
中的事件,示例:
1 | void main(){ |
基于以上事件模型的基础上,看下Future提供的几种构造函数,其中最基本的为直接传入一个Function
:
1 | factory Future(FutureOr<T> computation()) { |
Function
有多种写法:
1 | //简单操作,单步 |
Future.microtask
此工程方法创建的事件将发送到微任务队列Microtask Queue
,具有相比事件队列Event Queue
优先执行的特点
1 | factory Future.microtask(FutureOr<T> computation()) { |
Future.sync
返回一个立即执行传入参数的Future,可理解为同步调用
1 | factory Future.sync(FutureOr<T> computation()) { |
1 | Future.microtask(() => print(9)); |
Future.value
创建一个将来包含value的future
1 | factory Future.value([FutureOr<T>? value]) { |
参数FutureOr含义为T value 和 Future
1 | Future.value(12).then((value) => print(value)); |
这里需要注意即使value接收的是12,仍然会将事件发送到Event队列等待执行,但是相对其他Future事件执行顺序会提前
Future.error
创建一个执行结果为error的future
1 | factory Future.error(Object error, [StackTrace? stackTrace]) { |
1 | Future.error(new Exception("err msg")) |
Future.delayed
创建一个延迟执行回调的future,内部实现为Timer加延时执行一个Future
1 | factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) { |
Future.wait
等待多个Future并收集返回结果
1 | static Future<List<T>> wait<T>(Iterable<Future<T>> futures, |
FutureBuilder结合使用:
1 | child: FutureBuilder( |
Future.any
返回futures集合中第一个返回结果的值
1 | static Future<T> any<T>(Iterable<Future<T>> futures) { |
对上述例子来说,Future.any
snapshot.data
将返回firstFuture
、secondFuture
中第一个返回结果的值
Future.forEach
为传入的每一个元素,顺序执行一个action
1 | static Future forEach<T>(Iterable<T> elements, FutureOr action(T element)) { |
这里边action是方法作为参数,头一次见这种形式语法还是在js中,当时就迷惑了很大一会儿,使用示例:
1 | Future.forEach(["one","two","three"], (element) { |
Future.doWhile
执行一个操作直到返回false
1 | Future.doWhile((){ |
以上为Future中常用构造函数和方法
在Widget中使用Future
Flutter提供了配合Future显示的组件FutureBuilder
,使用也很简单,伪代码如下:
1 | child: FutureBuilder( |
Async-await
使用
这两个关键字提供了异步方法的同步书写方式,Future提供了方便的链式调用使用方式,但是不太直观,而且大量的回调嵌套造成可阅读性差。因次,现在很多语言都引入了await-async语法,学习他们的使用方式是很有必要的。
两条基本原则:
- 定义一个异步方法,必须在方法体前声明 async
- await关键字必须在async方法中使用
首先,在要执行耗时操作的方法体前增加async:
1 | void main() async { ··· } |
然后,根据方法的返回类型添加Future修饰
1 | Future<void> main() async { ··· } |
现在就可以使用await关键字来等待这个future执行完毕
1 | print(await createOrderMessage()); |
例如实现一个由一级分类获取二级分类,二级分类获取详情的需求,使用链式调用的代码如下:
1 | var list = getCategoryList(); |
现在来看下使用async/await,事情变得简单了多少
1 | Future<void> main() async { |
可以看到这样更加直观
缺陷
async/await 非常方便,但是还是有一些缺点需要注意
因为它的代码看起来是同步的,所以是会阻塞后面的代码执行,直到await返回结果,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但后边自己的代码被阻塞。
这意味着代码可能会由于有大量await代码相继执行而阻塞,本来用Future编写表示并行的操作,现在使用await变成了串行,例如,首页有一个同时获取轮播接口,tab列表接口,msg列表接口的需求
1 | Future<String> getBannerList() async { |
使用await编写很可能会写成这样,打印执行操作的时间
1 | Future<void> main2() async { |
在这里,我们直接等待所有三个模拟接口的调用,使每个调用3s。后续的每一个都被迫等到上一个完成, 最后会看到总运行时间为9s,而实际我们想三个请求同时执行,代码可以改成如下这种:
1 | Future<void> main() async { |
将三个Future存储在变量中,这样可以同时启动,最后打印时间仅为3s,所以在编写代码时,我们必须牢记这点,避免性能损耗。
原理
线程模型
当一个Flutter应用或者Flutter Engine启动时,它会启动(或者从池中选择)另外三个线程,这些线程有些时候会有重合的工作点,但是通常,它们被称为UI线程
,GPU线程
,IO线程
。需要注意一点这个UI线程并不是程序运行的主线程,或者说和其他平台上的主线程理解不同,通常的,Flutter将平台的主线程叫做”Platform thread”
UI线程是所有的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等事件,所以要谨慎使用,如下为流程图:
这两个任务队列中的任务切换在某些方面就相当于是协程调度机制
协程
协程是一种协作式的任务调度机制,区别于操作系统的抢占式任务调度机制,它是用户态下面的,避免线程切换的内核态、用户态转换的性能开销。它让调用者自己来决定什么时候让出cpu,比操作系统的抢占式调度所需要的时间代价要小很多,后者为了恢复现场会保存相当多的状态(不仅包括进程上下文的虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态),并且会频繁的切换,以现在流行的大多数Linux机器来说,每一次的上下文切换要消耗大约1.2-1.5μs的时间,这是仅考虑直接成本,固定在单个核心以避免迁移的成本,未固定情况下,切换时间可达2.2μs

对cpu来说这算一个很长的时间吗,一个很好的比较是memcpy
,在相同的机器上,完成一个64KiB数据的拷贝需要3μs的时间,上下文的切换比这个操作稍微快一些

协程和线程非常相似,是从异步执行任务的角度来看,而并不是从设计的实体角度像进程->线程->协程这样类似于细胞->原子核->质子中子这样的关系。可以理解为线程上执行的一段函数,用yield完成异步请求、注册回调/通知器、保存状态,挂起控制流、收到回调/通知、恢复状态、恢复控制流的所有过程
多线程执行任务模型如图:
线程的阻塞要靠系统间进程的切换,完成逻辑流的执行,频繁的切换耗费大量资源,而且逻辑流的执行数量严重依赖于程序申请到的线程的数量。
协程是协同多任务的,这意味着协程提供并发性但不提供并行性,执行流模型图如下:
协程可以用逻辑流的顺序去写控制流,协程的等待会主动释放cpu,避免了线程切换之间的等待时间,有更好的性能,逻辑流的代码编写和理解上也简单的很多
但是线程并不是一无是处,抢占式线程调度器事实上提供了准实时的体验。例如Timer,虽然不能确保在时间到达的时候一定能够分到时间片运行,但不会像协程一样万一没有人让出时间片就永远得不到运行……
总结
- 同步与异步
- Future提供了Flutter中异步代码链式编写方式
- async-wait提供了异步代码的同步书写方式
- Future的常用方法和FutureBuilder编写UI
- Flutter中线程模型,四个线程
- 单线程语言的事件驱动模型
- 进程间切换和协程对比
参考
https://dart.cn/tutorials/language/futures
https://dart.cn/codelabs/async-await
https://juejin.cn/post/6844903620064837645
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
https://www.zhihu.com/question/19732473
https://www.zhihu.com/question/50185085/answer/183463734
https://en.wikipedia.org/wiki/Asynchrony_(computer_programming)