42、Flutter之 Dart中的异步编程——Future、async和await
异步编程基本概念:
1、任务调度
先谈谈任务调度 ,大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式,对于单核CPU来讲,并行执行两个任务,实际上是CPU在进行着快速的切换,对用户来讲感觉不到有切换停顿,就好比220V交流电灯光显示原理一样,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。
任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来,任务的停与执行切换,称之为任务调度。
2、进程
计算机的核心是CPU,它承担了所有的计算任务,而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,操作系统中运行着多个进程,每一个进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是应用程序运行的载体。
操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位,也就是操作系统的最小单位。
3、线程
线程是进程中的概念,一个进程中可包含多个线程。
任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。
默认情况下,一般一个进程里只有一个线程,进程本身就是线程,所以线程可以被称为轻量级进程。
4、协程
协程,是一种基于线程,但又比线程更加轻量级的存在,是线程中的概念,一个线程可以拥有多个协程。
在传统的J2EE体系中都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如果遇到很耗时的I/O行为,则整个系统的吞吐立刻下降,因为这个时候线程一直处于阻塞状态,如果线程很多的时候,会存在很多其他的线程处于等待,空闲状态(等待前面的线程执行完才能执行),造成了资源应用不彻底。
最常见的例子就是同步阻塞的JDBC,在连接过程中线程根本没有利用CPU去做运算,而是处在等待状态,而另外过多的线程,也会带来更多的ContextSwitch(上下文切换)开销。
协程的出现,当出现长时间的I/O操作时,通过让出当前占用的任务通道,执行下一个任务的方式,通过在线程中实现调度,来消除ContextSwitch上的开销,避免了陷入内核级别的上下文切换造成的性能损失,进而突破了线程在IO上的性能瓶颈。从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制。
Flutter中的异步编程
Dart是基于单线程模型的语言,所以在Flutter中我们一般的异步操作,实际上还是通过单线程通过调度任务优先级来实现的。
在Dart中的线程机制,称为isolate,在Flutter项目中, 运行中的 Flutter 程序由一个或多个 isolate 组成,默认情况下启动的Flutter项目,通过main函数启动就是创建了一个main isolate,后续会有专门一文来论述isolate的开发使用,在这里我们 main isolate 为Flutter的主线程,或者是UI线程。
单线程模型中主要就是在维护着一个事件循环(Event Loop) 与 两个队列(event queue和microtask queue)
当Flutter项目程序触发如点击事件、IO事件、网络事件时,它们就会被加入到eventLoop中,eventLoop一直在循环之中,当主线程发现事件队列不为空时发现,就会取出事件,并且执行。
microtask queue只处理在当前 isolate 中的任务,优先级高于event queue,好比机场里的某个VIP候机室,总是VIP用户先登机了,才开放公共排队入口,如果在event事件队列中插入microtask,当当前event执行完毕即可插队执行microtask事件,microtask queue队列的存在为Dart提供了给任务队列插队的解决方案。
当事件循环正在处理microtask事件时的时候,event queue会被堵塞。这时候app就无法进行UI绘制,响应鼠标事件和I/O等事件。
Dart中异步可以异步的来执行耗时操作。从而可以在等待一个操作完成的同时进行别的操作以下是一些常见的异步操作:
- 通过网络获取数据。
- 写入数据库。
- 从文件读取数据。
要在Dart中执行异步操作,可以使用Future类和async和await关键字。
关键字async和await是Dart语言异步支持的一部分。
异步函数即在函数头中包含关键字async的函数。
- async:用来表示函数是异步的,定义的函数会返回一个Future对象。
- await:后面跟着一个Future,表示等待该异步任务完成,异步任务完成后才会继续往下执行。await只能出现在异步函数内部。能够让我们可以像写同步代码那样来执行异步任务而不使用回调的方式。
Dart的事件循环(event loop)
在Dart中,实际上有两种队列:
- 事件队列(
event queue),包含所有的外来事件:I/O、mouse events、drawing events、timers、isolate之间的信息传递。 - 微任务队列(
microtask queue),表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue添加的任务主要是由Dart内部产生。
因为
microtask queue的优先级高于event queue,所以如果microtask queue有太多的微任务, 那么就可能会霸占住当前的event loop。从而对event queue中的触摸、绘制等外部事件造成阻塞卡顿。
在每一次事件循环中,Dart总是先去第一个microtask queue中查询是否有可执行的任务,如果没有,才会处理后续的event queue的流程。

异步任务我们用的最多的还是优先级更低的 event queue。Dart为 event queue 的任务建立提供了一层封装,就是我们在Dart中经常用到的Future。
正常情况下,一个 Future 异步任务的执行是相对简单的:
- 声明一个
Future时,Dart会将异步任务的函数执行体放入event queue,然后立即返回,后续的代码继续同步执行。 - 当同步执行的代码执行完毕后,
event queue会按照加入event queue的顺序(即声明顺序),依次取出事件,最后同步执行Future的函数体及后续的操作。
Future
Future<T> 类,其表示一个 T 类型的异步操作结果。如果异步操作不需要结果,则类型为 Future<void>。也就是说首先Future是个泛型类,可以指定类型。如果没有指定相应类型的话,则Future会在执行动态的推导类型。
Future基本用法
Widget build(BuildContext context) {
return Scaffold(
appBar: getAppBar("async"),
body: ElevatedButton(onPressed: (){
test();
print("执行其他任务");
} , child: Text("测试异步")),
);
}
void test()async{
print("赋值任务开始");
for(var i=0;i<100000000;i++ ){
_data = i.toString();
}
print("赋值任务结束");
}
执行执行结果:

从结果可以看出执行是同步的,async只是告诉外部是一个异步方法,实际并没有加入到异步队列任务,如果要使用真正的异步,需要Future。
重新修改异步方法:
void test()async{
print("赋值任务开始");
await Future((){
for(var i=0;i<10000000000;i++ ){
_data = i.toString();
}
});
print("赋值任务结束");
}
从运行结果可以看出加上 await Future以后遇到耗时任务时候切换先执行了其他任务,最后再切换执行完成耗时操作。
**Flutter提供了下面三个方法,让我们来注册回调,来监听处理Future异步信息的结果: **


创建一个指定返回值的Future:
Future.value("sucess").then((value) => (debugPrint("测试$value")));

不需要等待结果的可以不使用await,await会阻塞代码向下执行,可以使用.then接受回调数据即可。如下图:

Future捕获异常
异步的异常捕获,Future.then除了onValue还有一个接受错误的方法,源码如下:
Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});


catchError一般放在then和complete后面。否则调用异常后还会走到then出现问题,Future比较完整的链式调用方式:
//Future。then的使用
Future future = Future((){
//耗时操作
for(var i=0;i<100000000;i++ ){
_data = i.toString();
}
throw Exception("假如程序出错");
return "dddd";
});
//使用then来接收数据
future.then((value){
print("好事任务执行结束:$value");
}).whenComplete((){
debugPrint("完成了任务");
}).catchError((error){
print(error.toString());
});
多个异步执行,Future任务会被添加到异步队列,然后依次顺序执行:
void testfutureMul(){
Future((){
return "task 1";
}).then((value) => print('$value 结束'));
Future((){
return "task 2";
}).then((value) => print('$value 结束'));
Future((){
return "task 3";
}).then((value) => print('$value 结束'));
Future((){
return "task 4";
}).then((value) => print('$value 结束'));
print("任务添加完毕");
}
上面例子可以看到首先主队列一次执行,添加任务1-4到异步队列,然后打印任务添加完成,异步中任务再继续执行,异步只是将任务添加到了另外的一个异步队列中,执行顺序还是依次执行,不会无序的,只有完成一个任务才能执行下一个任务。
如果多个异步任务结果需要依赖,可以再执行when回调后return结果并且继续后.then执行下一个任务一次依赖。

Future.delayed
创建一个延迟执行的 Future:
//创建一个延迟执行的 Future:
//延时三秒执行
Future.delayed(Duration(seconds: 3),(){
debugPrint("future delayed");
});
debugPrint("完成");
运行结果:

Future.forEach
根据某个集合,创建一系列的Future,并且会按顺序执行这些Future:
//根据某个集合,创建一系列的Future,并且会按顺序执行这些Future
Future.forEach([1,2,3,4], (element){
return Future.delayed(Duration(seconds: 3),(){
print(element);
});
});
debugPrint("完成");
运行结果:

Future.wait
串行执行多个异步任务,多个异步执行完成后一并去处理的情况.then拿到的结果是一个结果数组: :
//串行执行多个任务
var f1 = Future.delayed(const Duration(seconds: 1),()=>(1));
var f2 = Future.delayed(const Duration(seconds: 2),()=>(2));
var f3 = Future.delayed(const Duration(seconds: 3),()=>(3));
var f4 = Future.delayed(const Duration(seconds: 4),()=>(4));
Future.wait([f1,f2,f3,f4]).then((value) => print(value)).catchError(print);
debugPrint("完成");
执行结果:

Future.sync
会同步执行其入参函数,然后调度到microtask queue来完成自己。也就是一个阻塞任务,会阻塞当前代码,sync的任务执行完了,代码才能走到下一行:
void testFuture() async {
Future((){
print("Future event 1");
});
Future.sync(() {
print("Future sync microtask event 2");
});
Future((){
print("Future event 3");
});
Future.microtask((){
print("microtask event");
});
}
testFuture();
print("在testFuture()执行之后打印。");
执行结果:
Future sync microtask event 2
在testFuture()执行之后打印。
microtask event
Future event 1
Future event 3
在上述创建的异步任务都是添加到event队列中的任务,创建一个在microtask队列运行的future,microtask队列的优先级是比event队列高的。
Future工厂构造函数
工厂构造函数是一种构造函数,与普通构造函数不同,工厂函数不会自动生成实例,而是通过代码来决定返回的实例对象。
在Dart中,工厂构造函数的关键字为factory。我们知道,构造函数包含类名构造函数和命名构造方法,在构造方法前加上factory之后变成了工厂构造函数。也就是说factory可以放在类名函数之前,也可以放在命名函数之前。
factory Future(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
Timer.run(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
转载自:https://juejin.cn/post/7171646263283171341