阅读 298

Flutter中Dart异步模型

前言

我们知道Flutter 框架有出色的渲染和交互能力。支撑起这些复杂的能力背后,实际上是基于单线程模型的 Dart。那么,与原生 Android 和 iOS 的多线程机制相比,单线程的 Dart 如何从语言设计层面和代码运行机制上保证 Flutter UI 的流畅性呢?

单线程模型

我们从下面几个方面阐述一下:

  1. Dart 语言单线程模型和 Event Loop 处理机制
  2. 异步处理和并发编程的原理和使用方法
  3. Dart 单线程模型下的代码运行本质

1. Dart单线程模型

dart是单线程运行的。怎么理解这句话呢, 从下面几个方面可以看到这个设计思想.

1.1 默认单一运行的线程

dart默认运行在Main函数存在线程,在dart中称之为isolate,这个线程我们可称之为main isolate。单线程任务处理的,如果不开启新的isolate,任务默认在主isolate中处理。一旦 Dart 函数执行,它将按照在 main 函数出现的次序一个接一个地持续执行,直到退出。换而言之,Dart 函数在执行期间,无法被其他 Dart 代码打断。

1.2 独享内存

Android和IOS可以自由的开辟除了UI主线程之外的线程,这些线程和主线程可以共享内存的变量,但是, Dart中的isolate无法共享内存。Isolate 不能共享内存,他们就像是单独的分离的 app,通过消息进行沟通。除了显式指定代码运行在别的 isolate 或者 worker 中,其他代码都运行在 app 的 main isolate 中。更多信息可以访问Use isolates or workers if necessary

1.3 质疑

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

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

(2)单线程模型是指的事件队列模型,和绘制界面的线程是一个吗?

答:我们所说的单线程指的是主Isolate。而GPU绘制指令有单独的线程执行,跟主Isolate无关。事实上Flutter提供了4种task runner,有独立的线程去运行专属的任务:参见:深入理解Flutter引擎线程模式

  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任务

2. Event Loop 机制

消息队列模型

如图所示,dart也存在事件队列和事件循环。每个isolate也包含一个事件循环,区别是他有两个事件队列,event loop事件循环,以及event queue和microtask queue事件队列,event和microtask队列有点类似iOS的source0和source1。

  • event queue:负责处理I/O事件、绘制事件、手势事件、接收其他isolate消息等外部事件。
  • microtask queue:可以自己向isolate内部添加事件,事件的优先级比event queue高。
事件队列模型
  1. 先检查MicroTask队列是否为空,非空则先执行MicroTask队列中的MicroTask
  2. 一个MicroTask执行完后,检查有没有下一个MicroTask,直到MicroTask队列为空,才去执行Event队列
  3. Evnet 队列取出一个事件处理完后,再次返回第一步,去检查MicroTask队列是否为空

我们可以看出,将任务加入到MicroTask中可以被尽快执行,但也需要注意,当事件循环在处理MicroTask队列时,会阻塞event队列的事件执行,这样就会导致渲染、手势响应等event事件响应延时。为了保证渲染和手势响应,应该尽量将耗时操作放在event队列中。

我们通常很少会直接用到微任务队列,就连 Flutter 内部,也只有 7 处用到了而已(比如,手势识别、文本输入、滚动视图、保存页面效果等需要高优执行任务的场景)。

简单总结为一二一模型:1个事件循环和2个队列的单线程执行模型。

3. 异步任务调度

为什么单线程也可以异步?这里有一个大前提,那就是我们的 App 绝大多数时间都在等待。比如,等用户点击、等网络请求返回、等文件 IO 结果,等等。而这些等待行为并不是阻塞的。比如说,网络请求,Socket 本身提供了 select 模型可以异步查询;而文件 IO,操作系统也提供了基于事件的回调机制。所以,基于这些特点,单线程模型可以在等待的过程中做别的事情,等真正需要响应结果了,再去做对应的处理。因为等待过程并不是阻塞的,所以给我们的感觉就像是同时在做多件事情一样。但其实始终只有一个线程在处理你的事情。

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

3.1 用Future发起异步任务

Dart 为 Event Queue 的任务建立提供了一层封装,叫作 Future。Future 还提供了链式调用的能力,可以在异步任务执行完毕后依次执行链路上的其他函数体。

new Future((){
    //  doing something
});复制代码

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

scheduleMicrotask(() => print('This is a microtask'));复制代码

链式调用:

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

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

Dart 会将异步任务的函数执行体放入事件队列,然后立即返回,后续的代码继续同步执行。而当同步执行的代码执行完毕后,事件队列会按照加入事件队列的顺序(即声明顺序),依次取出事件,最后同步执行 Future 的函数体及后续的 then。这意味着,then 与 Future 函数体共用一个事件循环。而如果 Future 有多个 then,它们也会按照链式调用的先后顺序同步执行,同样也会共用一个事件循环。

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

//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 f3 then 3 then 4复制代码

4. 异步函数

Future 是异步任务的封装,借助于 await 与 async,我们可以通过事件循环实现非阻塞的同步等待。Dart 中的 await 并不是阻塞等待,而是异步等待。Dart 会将调用体的函数也视作异步函数,将等待语句的上下文放入 Event Queue 中,一旦有了结果,Event Loop 就会把它从 Event Queue 中取出,等待代码继续执行。

async关键字作为方法声明的后缀时,具有如下意义

  • 被修饰的方法会将一个 Future 对象作为返回值
  • 该方法会同步执行其中的方法的代码直到第一个 await 关键字,然后它暂停该方法其他部分的执行;
  • 一旦由 await 关键字引用的 Future 任务执行完成,await的下一行代码将立即执行。
// 导入io库,调用sleep函数
import 'dart:io';

// 模拟耗时操作,调用sleep函数睡眠2秒
doTask() async{
  await sleep(const Duration(seconds:2));
  return "Ok";
}

// 定义一个函数用于包装
test() async {
  var r = await doTask();
  print(r);
}

void main(){
  print("main start");
  test();
  print("main end");
}
结果:
main start
main end
Ok复制代码

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

Future(()=>print('f1'))
.then((_)async=>awaitFuture(()=>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 并不会被它阻塞。

5. Isolate

Dart 也提供了多线程机制,即 Isolate(这个单词的中文意思是隔离)。在 Isolate 中,资源隔离做得非常好,每个 Isolate 都有自己的 Event Loop 与 Queue,Isolate 之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。如下所示,我们声明了一个 Isolate 的入口函数,然后在 main 函数中启动它,并传入了一个字符串参数:

doSth(msg) => print(msg);

main() {
  Isolate.spawn(doSth, "Hi");
  ...
}复制代码

那么如何利用消息机制进行通信呢,下面引用了一篇文章的讲解,图画的很好。

引用

整个消息通信过程如上图所示,两个Isolate是通过两对Port对象通信,一对Port分别由用于接收消息的ReceivePort对象,和用于发送消息的SendPort对象构成。其中SendPort对象不用单独创建,它已经包含在ReceivePort对象之中。需要注意,一对Port对象只能单向发消息,这就如同一根自来水管,ReceivePortSendPort分别位于水管的两头,水流只能从SendPort这头流向ReceivePort这头。因此,两个Isolate之间的消息通信肯定是需要两根这样的水管的,这就需要两对Port对象。

6. 引用文章

(1)23 | 单线程模型怎么保证UI运行流畅?

(2)Dart 异步编程详解之一文全懂

(3)Dart asynchronous programming: Isolates and event loops


关注下面的标签,发现更多相似文章
评论