Flutter之Future 异步await sync 终结者

20年圣诞

2020 神奇的一年,载入史册的一年,改变了很多人的命运,曼谷第一开膛手也转行入坑Flutter,废话不多说,干就完了

Event Loop 机制

像iOS runloop一样,flutter也有事件循环,不同的是,那就是 Dart 是单线程的。那单线程意味着什么呢?这意味着 Dart 代码是有序的,按照在 main 函数出现的次序一个接一个地执行,不会被其他代码中断。另外,作为支持 Flutter 这个 UI 框架的关键技术,Dart 当然也支持异步。需要注意的是,单线程和异步并不冲突。

为什么单线程也可以异步?

比如说,网络请求,Socket 本身提供了 select 模型可以异步查询;而文件 IO,操作系统也提供了基于事件的回调机制。

基于这些特点,单线程模型可以在等待的过程中做别的事情,等真正需要响应结果了,再去做对应的处理。因为等待过程并不是阻塞的,所以给我们的感觉就像是同时在做多件事情一样。但其实始终只有一个线程在处理你的事情。

等待这个行为是通过 Event Loop 驱动的。事件队列 Event Queue 会把其他平行世界(比如 Socket)完成的,需要主线程响应的事件放入其中。像其他语言一样,Dart 也有一个巨大的事件循环,在不断的轮询事件队列,取出事件(比如,键盘事件、I\O 事件、网络事件等),在主线程同步执行其回调函数,如下图所示:

Event Loop
异步任务

图 1 的 Event Loop 示意图只是一个简化版。在 Dart 中,实际上有两个队列,一个事件队列(Event Queue),另一个则是微任务队列(Microtask Queue)。在每一次事件循环中,Dart 总是先去第一个微任务队列中查询是否有可执行的任务,有的话会处理完所有的任务。如果没有,才会处理后续的事件队列的流程。

Event Loop 完整版的流程图,应该如下所示:


Microtask Queue 与 Event Queue

首先,我们看看微任务队列。微任务顾名思义,表示一个短时间内就会完成的异步任务。从上面的流程图可以看到,微任务队列在事件循环中的优先级是最高的,只要队列中还有任务,就可以一直霸占着事件循环。

微任务是由 scheduleMicroTask 建立的。如下所示,这段代码会在下一个事件循环中输出一段字符串:


scheduleMicrotask(() => print('This is a microtask'));

一般的异步任务通常也很少必须要在事件队列前完成,所以也不需要太高的优先级,因此我们通常很少会直接用到微任务队列,就连 Flutter 内部,也只有 7 处用到了而已(比如,手势识别、文本输入、滚动视图、保存页面效果等需要高优执行任务的场景)。

异步任务我们用的最多的还是优先级更低的 Event Queue。比如,I/O、绘制、定时器这些异步事件,都是通过事件队列驱动主线程执行的。

Dart 为 Event Queue 的任务建立提供了一层封装,叫作 Future。从名字上也很容易理解,它表示一个在未来时间才会完成的任务。

把一个函数体放入 Future,就完成了从同步任务到异步任务的包装。Future 还提供了链式调用的能力,可以在异步任务执行完毕后依次执行链路上的其他函数体。

接下来,我们看一个具体的代码示例:分别声明两个异步任务,在下一个事件循环中输出一段字符串。其中第二个任务执行完毕之后,还会继续输出另外两段字符串:


Future(() => print('Running in Future 1'));//下一个事件循环输出字符串

Future(() => print(‘Running in Future 2'))
  .then((_) => print('and then 1'))
  .then((_) => print('and then 2’));//上一个事件循环结束后,连续输出三段字符串

当然,这两个 Future 异步任务的执行优先级比微任务的优先级要低。

正常情况下,一个 Future 异步任务的执行是相对简单的:在我们声明一个 Future 时,Dart 会将异步任务的函数执行体放入事件队列,然后立即返回,后续的代码继续同步执行。而当同步执行的代码执行完毕后,事件队列会按照加入事件队列的顺序(即声明顺序),依次取出事件,最后同步执行 Future 的函数体及后续的 then。

这意味着,then 与 Future 函数体共用一个事件循环。而如果 Future 有多个 then,它们也会按照链式调用的先后顺序同步执行,同样也会共用一个事件循环。

如果 Future 执行体已经执行完毕了,但你又拿着这个 Future 的引用,往里面加了一个 then 方法体,这时 Dart 会如何处理呢?面对这种情况,Dart 会将后续加入的 then 方法体放入微任务队列,尽快执行。

下面的代码演示了 Future 的执行规则,即,先加入事件队列,或者先声明的任务先执行;then 在 Future 结束后立即执行。


//f1比f2先执行
Future(() => print('f1'));
Future(() => print('f2'));

//f3执行后会立刻同步执行then 3
Future(() => print('f3')).then((_) => print('then 3'));

//then 4会加入微任务队列,尽快执行
Future(() => null).then((_) => print('then 4'));
  • 在第一个例子中,由于 f1 比 f2 先声明,因此会被先加入事件队列,所以 f1 比 f2 先执行;
  • 在第二个例子中,由于 Future 函数体与 then 共用一个事件循环,因此 f3 执行后会立刻同步执行 then 3;
  • 最后一个例子中,Future 函数体是 null,这意味着它不需要也没有事件循环,因此后续的 then 也无法与它共享。在这种场景下,Dart 会把后续的 then 放入微任务队列,在下一次事件循环中执行。

通过一个综合案例,来把之前介绍的各个执行规则都串起来,再集中学习一下。
在下面的例子中,我们依次声明了若干个异步任务 Future,以及微任务。在其中的一些 Future 内部,我们又内嵌了 Future 与 microtask 的声明:


Future(() => print('f1'));//声明一个匿名Future
Future fx = Future(() =>  null);//声明Future fx,其执行体为null

//声明一个匿名Future,并注册了两个then。在第一个then回调里启动了一个微任务
Future(() => print('f2')).then((_) {
  print('f3');
  scheduleMicrotask(() => print('f4'));
}).then((_) => print('f5'));

//声明了一个匿名Future,并注册了两个then。第一个then是一个Future
Future(() => print('f6'))
  .then((_) => Future(() => print('f7')))
  .then((_) => print('f8'));

//声明了一个匿名Future
Future(() => print('f9'));

//往执行体为null的fx注册了了一个then
fx.then((_) => print('f10'));

//启动一个微任务
scheduleMicrotask(() => print('f11'));
print('f12');

先别急往下看结果,自己在小本本上写写,或者自己敲代码运行一下

运行一下,上述各个异步任务会依次打印其内部执行结果:


f12
f11
f1
f10
f2
f3
f5
f4
f6
f9
f7
f8

看到这儿,你可能已经懵了。别急,我们先来看一下这段代码执行过程中,Event Queue 与 Microtask Queue 中的变化情况,依次分析一下它们的执行顺序为什么会是这样的:

Event Queue 与 Microtask Queue 变化示例
  • 因为其他语句都是异步任务,所以先打印 f12。
  • 剩下的异步任务中,微任务队列优先级最高,因此随后打印 f11;然后按照 Future 声明的先后顺序,打印 f1。
  • 随后到了 fx,由于 fx 的执行体是 null,相当于执行完毕了,Dart 将 fx 的 then 放入微任务队列,由于微任务队列的优先级最高,因此 fx 的 then 还是会最先执行,打印 f10。
  • 然后到了 fx 下面的 f2,打印 f2,然后执行 then,打印 f3。f4 是一个微任务,要到下一个事件循环才执行,因此后续的 then 继续同步执行,打印 f5。本次事件循环结束,下一个事件循环取出 f4 这个微任务,打印 f4。
  • 然后到了 f2 下面的 f6,打印 f6,然后执行 then。这里需要注意的是,这个 then 是一个 Future 异步任务,因此这个 then,以及后续的 then 都被放入到事件队列中了。后续的then 也被放到事件队列 是因为 箭头函数 return了个future,所以后续的then是跟着他的,如果没有return future的话,then 会跟着前面的同步执行
  • f6 下面还有 f9,打印 f9。
  • 最后一个事件循环,打印 f7,以及后续的 f8。

你只需要记住一点:then 会在 Future 函数体执行完毕后立刻执行,无论是共用同一个事件循环还是进入下一个微任务

在深入理解 Future 异步任务的执行规则之后,我们再来看看怎么封装一个异步函数。

异步函数

对于一个异步函数来说,其返回时内部执行动作并未结束,因此需要返回一个 Future 对象,供调用者使用。调用者根据 Future 对象,来决定:是在这个 Future 对象上注册一个 then,等 Future 的执行体结束了以后再进行异步处理;还是一直同步等待 Future 执行体结束。

对于异步函数返回的 Future 对象,如果调用者决定同步等待,则需要在调用处使用 await 关键字,并且在调用处的函数体使用 async 关键字。

在下面的例子中,异步方法延迟 3 秒返回了一个 Hello 2019,在调用处我们使用 await 进行持续等待,等它返回了再打印:


//声明了一个延迟3秒返回Hello的Future,并注册了一个then返回拼接后的Hello 2019
Future<String> fetchContent() => 
  Future<String>.delayed(Duration(seconds:3), () => "Hello")
    .then((x) => "$x 2019");

  main() async{
  String str = await fetchContent()
    print(str);//等待Hello 2019的返回
  }

也许你已经注意到了,我们在使用 await 进行等待的时候,在等待语句的调用上下文函数 main 加上了 async 关键字。为什么要加这个关键字呢?

因为 Dart 中的 await 并不是阻塞等待,而是异步等待。Dart 会将调用体的函数也视作异步函数,将等待语句的上下文放入 Event Queue 中,一旦有了结果,Event Loop 就会把它从 Event Queue 中取出,等待代码继续执行。

我们先来看下这段代码。第二行的 then 执行体 f2 是一个 Future,为了等它完成再进行下一步操作,我们使用了 await,期望打印结果为 f1、f2、f3、f4:


Future(() => print('f1'))
  .then((_) async => await Future(() => print('f2')))
  .then((_) => print('f3'));
Future(() => print('f4'));

实际上,当你运行这段代码时就会发现,打印出来的结果其实是 f1、f4、f2、f3!

分析一下这段代码的执行顺序:

  • 按照任务的声明顺序,f1 和 f4 被先后加入事件队列。
  • f1 被取出并打印;然后到了 then。then 的执行体是个 future f2,于是放入 Event Queue。然后把 await 也放到 Event Queue 里。
  • 这个时候要注意了,Event Queue 里面还有一个 f4,我们的 await 并不能阻塞 f4 的执行。因此,Event Loop 先取出 f4,打印 f4;然后才能取出并打印 f2,最后把等待的 await 取出,开始执行后面的 f3。

由于 await 是采用事件队列的机制实现等待行为的,所以比它先在事件队列中的 f4 并不会被它阻塞。

接下来,我们再看另一个例子:在主函数调用一个异步函数去打印一段话,而在这个异步函数中,我们使用 await 与 async 同步等待了另一个异步函数返回字符串:


//声明了一个延迟2秒返回Hello的Future,并注册了一个then返回拼接后的Hello 2019
Future<String> fetchContent() => 
  Future<String>.delayed(Duration(seconds:2), () => "Hello")
    .then((x) => "$x 2019");
//异步函数会同步等待Hello 2019的返回,并打印
func() async => print(await fetchContent());

main() {
  print("func before");
  func();
  print("func after");
}

运行这段代码,我们发现最终输出的顺序其实是“func before”“func after”“Hello 2019”。func 函数中的等待语句似乎没起作用。这是为什么呢?

我来给你分析一下这段代码的执行顺序:

  • 首先,第一句代码是同步的,因此先打印“func before”。
  • 然后,进入 func 函数,func 函数调用了异步函数 fetchContent,并使用 await 进行等待,因此我们把 fetchContent、await 语句的上下文函数 func 先后放入事件队列。
  • await 的上下文函数并不包含调用栈,因此 func 后续代码继续执行,打印“func after”。
  • 2 秒后,fetchContent 异步任务返回“Hello 2019”,于是 func 的 await 也被取出,打印“Hello 2019”。

通过上述分析,你发现了什么现象?那就是 await 与 async 只对调用上下文的函数有效,并不向上传递。因此对于这个案例而言,func 是在异步等待。如果我们想在 main 函数中也同步等待,需要在调用异步函数时也加上 await,在 main 函数也加上 async。

各位大佬 应该还是迷糊,多看几遍,多敲几遍 自然会懂得

总结

在 UI 编程过程中,异步和多线程是两个相伴相生的名词,也是很容易混淆的概念。对于异步方法调用而言,代码不需要等待结果的返回,而是通过其他手段(比如通知、回调、事件循环或多线程)在后续的某个时刻主动(或被动)地接收执行结果。

因此,从辩证关系上来看,异步与多线程并不是一个同等关系:异步是目的,多线程只是我们实现异步的一个手段之一。而在 Flutter 中,借助于 UI 框架提供的事件循环,我们可以不用阻塞的同时等待多个异步任务,因此并不需要开多线程。我们一定要记住这一点。

Q/A
  1. 假如有一个任务(读写文件或者网络)耗时10秒,并且加入到了事件任务队列中,执行单这个任务的时候不就把线程卡主吗?

文件I/O和网络调用并不是在Dart层做的,而是由操作系统提供的异步线程,他俩把活儿干完之后把结果刚到队列中,Dart代码只是执行一个简单的读动作。

  1. 单线程模型是指的事件队列模型,和绘制界面的线程是一个吗
    我们所说的单线程指的是主Isolate。而GPU绘制指令有单独的线程执行,跟主Isolate无关。事实上Flutter提供了4种task runner,有独立的线程去运行专属的任务:
    1.Platform Task Runner:处理来自平台(Android/iOS)的消息
    2.UI Task Runner:执行渲染逻辑、处理native plugin的消息、timer、microtask、异步I/O操作处理等
    3.GPU Task Runner:执行GPU指令
    4.IO Task Runner:执行I/O任务
    除此之外,操作系统本身也提供了大量异步并发机制,可以利用多线程去执行任务(比如socket),我们在主Isolate中无需关心(如果真想主动创建并发任务也可以)

// 第一段
  Future(() => print('f6'))
    .then((_) => Future(() => print('f7')))
    .then((_) => print('f8'));

执行结果为:f6 f7 f8

 // 第二段
  Future(() => print('f6'))
  .then((_) {
      Future(() => print('f7'));
    })
  .then((_) => print('f8'));

执行结果为:f6 f8 f7
上面这两段代码为什么执行结果不一样呢?

单行箭头函数是Future,和函数体里有Future不是一回事
then函数会返回future,所以用=>的写法时, 新创建的future会被then返回,第二个then的调用者就是新的future对象, 所以第二个then就不跟着原先的future 而是跟着它的新future了。
如果把=>换成大括号,此时第二个then还是跟着原先的future,和新future无关

  1. 下一个奇异的是什么时候

    2045年

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容