前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter中Dart异步模型

Flutter中Dart异步模型

原创
作者头像
jerrypxiao
修改2019-11-13 22:52:24
1.8K0
修改2019-11-13 22:52:24
举报
文章被收录于专栏:音视频专栏音视频专栏

前言

我们知道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 还提供了链式调用的能力,可以在异步任务执行完毕后依次执行链路上的其他函数体。

代码语言:javascript
复制
new Future((){
    //  doing something
});

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

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

链式调用:

代码语言:javascript
复制
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 方法体放入微任务队列,尽快执行。

代码语言:javascript
复制
//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的下一行代码将立即执行。
代码语言:javascript
复制
// 导入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:

代码语言:javascript
复制
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 函数中启动它,并传入了一个字符串参数:

代码语言:javascript
复制
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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 1. Dart单线程模型
    • 1.1 默认单一运行的线程
      • 1.2 独享内存
        • 1.3 质疑
        • 2. Event Loop 机制
        • 3. 异步任务调度
          • 3.1 用Future发起异步任务
          • 4. 异步函数
          • 5. Isolate
          • 6. 引用文章
          相关产品与服务
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档