Flutter中的异步
同步与异步
程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用户层面来说,我们可以选择stop the world,等待操作完成返回结果后再继续操作,也可以选择继续去执行其他操作,等事件返回结果后再通知回来。这就是从用户角度来看的同步与异步。
从操作系统的角度,同步异步,与任务调度,进程间切换,中断,系统调用之间有着更为复杂的关系。
同步I/O 与 异步I/O的区别
为什么使用异步
用户可以阻塞式的等待,因为人的操作和计算机相比是非常慢的,计算机如果阻塞那就是很大的性能浪费了,异步操作让您的程序在等待另一个操作的同时完成工作。三种异步操作的场景:
- I/O操作:例如:发起一个网络请求,读写数据库、读写文件、打印文档等,一个同步的程序去执行这些操作,将导致程序的停止,直到操作完成。更有效的程序会改为在操作挂起时去执行其他操作,假设您有一个程序读取一些用户输入,进行一些计算,然后通过电子邮件发送结果。发送电子邮件时,您必须向网络发送一些数据,然后等待接收服务器响应。等待服务器响应所投入的时间是浪费的时间,如果程序继续计算,这将得到更好的利用
- 并行执行多个操作:当您需要并行执行不同的操作时,例如进行数据库调用、Web 服务调用以及任何计算,那么我们可以使用异步
- 长时间运行的基于事件驱动的请求:这就是您有一个请求进来的想法,并且该请求进入休眠状态一段时间等待其他一些事件的发生。当该事件发生时,您希望请求继续,然后向客户端发送响应。所以在这种情况下,当请求进来时,线程被分配给该请求,当请求进入睡眠状态时,线程被发送回线程池,当任务完成时,它生成事件并从线程池中选择一个线程发送响应
计算机中异步的实现方式就是任务调度,也就是进程的切换
任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。
计算机系统分为用户空间
和内核空间
,用户进程在用户空间,操作系统运行在内核空间,内核空间的数据访问修改拥有高于普通进程的权限,用户进程之间相互独立,内存不共享,保证操作系统的运行安全。如何最大化的利用CPU,确定某一时刻哪个进程拥有CPU资源就是任务调度的过程。内核负责调度管理用户进程,以下为进程调度过程
在任意时刻, 一个 CPU 核心上(processor)只可能运行一个进程
每一个进程可以包含多个线程,线程是执行操作的最小单元,因此进程的切换落实到具体细节就是正在执行线程的切换
Future
Future<T> 表示一个异步的操作结果,用来表示一个延迟的计算,返回一个结果或者error
,使用代码实例:
Future<int> future = getFuture();
future.then((value) => handleValue(value))
.catchError((error) => handleError(error))
.whenComplete(func);
future可以是三种状态:未完成的
、返回结果值
、返回异常
当一个返回future对象被调用时,会发生两件事:
- 将函数操作入队列等待执行结果并返回一个未完成的Future对象
- 函数操作完成时,
Future
对象变为完成并携带一个值或一个错误
首先,Flutter事件处理模型为先执行main函数,完成后检查执行微任务队列Microtask Queue
中事件,最后执行事件队列Event Queue
中的事件,示例:
void main(){
Future(() => print(10));
Future.microtask(() => print(9));
print("main");
}
/// 打印结果为:
/// main
/// 9
/// 10
基于以上事件模型的基础上,看下Future提供的几种构造函数,其中最基本的为直接传入一个Function
:
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;
}
Function
有多种写法:
//简单操作,单步
Future(() => print(5));
//稍复杂,匿名函数
Future((){
print(6);
});
//更多操作,方法名
Future(printSeven);
printSeven(){
print(7);
}
Future.microtask
此工程方法创建的事件将发送到微任务队列Microtask Queue
,具有相比事件队列Event Queue
优先执行的特点
factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
//
scheduleMicrotask(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
Future.sync
返回一个立即执行传入参数的Future,可理解为同步调用
factory Future.sync(FutureOr<T> computation()) {
try {
var result = computation();
if (result is Future<T>) {
return result;
} else {
// TODO(40014): Remove cast when type promotion works.
return new _Future<T>.value(result as dynamic);
}
} catch (error, stackTrace) {
/// ...
}
}
Future.microtask(() => print(9));
Future(() => print(10));
Future.sync(() => print(11));
/// 打印结果: 11、9、10
Future.value
创建一个将来包含value的future
factory Future.value([FutureOr<T>? value]) {
return new _Future<T>.immediate(value == null ? value as T : value);
}
参数FutureOr含义为T value 和 Future value 的合集,因为对于一个Future参数来说,他的结果可能为value或者是Future,所以对于以下两种写法均合法:
Future.value(12).then((value) => print(value));
Future.value(Future<int>((){
return 13;
}));
这里需要注意即使value接收的是12,仍然会将事件发送到Event队列等待执行,但是相对其他Future事件执行顺序会提前
Future.error
创建一个执行结果为error的future
factory Future.error(Object error, [StackTrace? stackTrace]) {
/// ...
return new _Future<T>.immediateError(error, stackTrace);
}
_Future.immediateError(var error, StackTrace stackTrace)
: _zone = Zone._current {
_asyncCompleteError(error, stackTrace);
}
Future.error(new Exception("err msg"))
.then((value) => print("err value: $value"))
.catchError((e) => print(e));
/// 执行结果为:Exception: err msg
Future.delayed
创建一个延迟执行回调的future,内部实现为Timer加延时执行一个Future
factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
/// ...
new Timer(duration, () {
if (computation == null) {
result._complete(null as T);
} else {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
}
});
return result;
}
Future.wait
等待多个Future并收集返回结果
static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
{bool eagerError = false, void cleanUp(T successValue)?}) {
/// ...
}
FutureBuilder结合使用:
child: FutureBuilder(
future: Future.wait([
firstFuture(),
secondFuture()
]),
builder: (context,snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
}
final first = snapshot.data[0];
final second = snapshot.data[1];
return Text("data $first $second");
},
),
Future.any
返回futures集合中第一个返回结果的值
static Future<T> any<T>(Iterable<Future<T>> futures) {
var completer = new Completer<T>.sync();
void onValue(T value) {
if (!completer.isCompleted) completer.complete(value);
}
void onError(Object error, StackTrace stack) {
if (!completer.isCompleted) completer.completeError(error, stack);
}
for (var future in futures) {
future.then(onValue, onError: onError);
}
return completer.future;
}
对上述例子来说,Future.any
snapshot.data
将返回firstFuture
、secondFuture
中第一个返回结果的值
Future.forEach
为传入的每一个元素,顺序执行一个action
static Future forEach<T>(Iterable<T> 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
执行一个操作直到返回false
Future.doWhile((){
for(var i=0;i<5;i++){
print("i => $i");
if(i >= 3){
return false;
}
}
return true;
});
/// 结果打印到 3
以上为Future中常用构造函数和方法
在Widget中使用Future
Flutter提供了配合Future显示的组件FutureBuilder
,使用也很简单,伪代码如下:
child: FutureBuilder(
future: getFuture(),
builder: (context, snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return _ErrorWidget("Error: ${snapshot.error}");
} else {
return _ContentWidget("Result: ${snapshot.data}")
}
}
)
Async-await
使用
这两个关键字提供了异步方法的同步书写方式,Future提供了方便的链式调用使用方式,但是不太直观,而且大量的回调嵌套造成可阅读性差。因此,现在很多语言都引入了await-async语法,学习他们的使用方式是很有必要的。
两条基本原则:
- 定义一个异步方法,必须在方法体前声明 async
- await关键字必须在async方法中使用
首先,在要执行耗时操作的方法体前增加async:
void main() async { ··· }
然后,根据方法的返回类型添加Future修饰
Future<void> main() async { ··· }
现在就可以使用await关键字来等待这个future执行完毕
print(await createOrderMessage());
例如实现一个由一级分类获取二级分类,二级分类获取详情的需求,使用链式调用的代码如下:
var list = getCategoryList();
list.then((value) => value[0].getCategorySubList(value[0].id))
.then((subCategoryList){
var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
print(courseList);
}).catchError((e) => (){
print(e);
});
现在来看下使用async/await,事情变得简单了多少
Future<void> main() async {
await getCourses().catchError((e){
print(e);
});
}
Future<void> getCourses() async {
var list = await getCategoryList();
var subCategoryList = await list[0].getCategorySubList(list[0].id);
var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
print(courseList);
}
可以看到这样更加直观
缺陷
async/await 非常方便,但是还是有一些缺点需要注意
因为它的代码看起来是同步的,所以是会阻塞后面的代码执行,直到await返回结果,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但后边自己的代码被阻塞。
这意味着代码可能会由于有大量await代码相继执行而阻塞,本来用Future编写表示并行的操作,现在使用await变成了串行,例如,首页有一个同时获取轮播接口,tab列表接口,msg列表接口的需求
Future<String> getBannerList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "banner list";
});
}
Future<String> getHomeTabList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "tab list";
});
}
Future<String> getHomeMsgList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "msg list";
});
}
使用await编写很可能会写成这样,打印执行操作的时间
Future<void> main2() async {
var startTime = DateTime.now().second;
await getBannerList();
await getHomeTabList();
await getHomeMsgList();
var endTime = DateTime.now().second;
print(endTime - startTime); // 9
}
在这里,我们直接等待所有三个模拟接口的调用,使每个调用3s。后续的每一个都被迫等到上一个完成, 最后会看到总运行时间为9s,而实际我们想三个请求同时执行,代码可以改成如下这种:
Future<void> main() async {
var startTime = DateTime.now().second;
var bannerList = getBannerList();
var homeTabList = getHomeTabList();
var homeMsgList = getHomeMsgList();
await bannerList;
await homeTabList;
await homeMsgList;
var endTime = DateTime.now().second;
print(endTime - startTime); // 3
}
将三个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中线程模型,四个线程
- 单线程语言的事件驱动模型
- 进程间切换和协程对比