7.Flutte3.0 遥遥领先系列|一文教你完全掌握单线程&异

2024-02-01  本文已影响0人  鹏城十八少

目录:

  1. 单线程原理 Event Loop机制
  2. 异步任务Future以及async、await (减少嵌套, 异步代码同步写)
  3. Future的链式调用then
  4. 执行顺序和优先级
  5. 多线程: Isolate, Isolate双向通信和 compute函数的使用
  6. Platform view线程合并的问题。

1. 单线程原理 Event Loop机制

问题: 单线程是如何来处理网络通信、IO操作它们返回的结果呢?答案就是事件循环(Event Loop)

Event Loop机制

处理方式二: 单线程+事件循环,比如JavaScript、Dart都是基于单线程加事件循环来完成耗时操作的处理。不过单线程如何能进行耗时的操作呢?!
首先,我们需要建立这样一个概念,那就是Dart是单线程的。

单线程模型中主要就是在维护着一个事件循环(Event Loop)。
事实上事件循环并不复杂,它就是将需要处理的一系列事件(包括点击事件、IO事件、网络事件)放在一个事件队列(Event Queue)中。
不断的从事件队列(Event Queue)中取出事件,并执行其对应需要执行的代码块,直到事件队列清空位置

eventloop.jpg

问题: 那单线程意味着什么呢?

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

问题: 那为什么单线程也可以异步?

阻塞式调用和非阻塞式调用
执行网络, 文件的, 操作系统会异步调用, 称为非阻塞调用, 我们使用的时候, 传入一个callback即可!

我们用一个生活中的例子来模拟:

你中午饿了,需要点一份外卖,点外卖的动作就是我们的调用,拿到最后点的外卖就是我们要等待的结果。
阻塞式调用: 点了外卖,不再做任何事情,就是在傻傻的等待,你的线程停止了任何其他的工作。
非阻塞式调用: 点了外卖,继续做其他事情:继续工作、打把游戏,你的线程没有继续执行其他事情,只需要偶尔去看一下有没有人敲门,外卖有没有送到即可。

单线程模型的优点

解决这一问题的典型方法是,使用锁来保护共享资源,但锁本身又可能会带来性能损耗,甚至出现死锁等更严重的问题。

这时,Dart 是单线程模型的优势就体现出来了,因为它天然不存在资源竞争和状态同步的问题

我们介绍了很多Flutter框架出色的渲染和交互能力。支撑起这些复杂的能力背后,实际上是基于单线程模型的Dart。

2. 异步任务Future以及async、await

在Dart中,实际上有两个队列,一个事件队列(Event Queue),另一个则是微任务队列(Microtask Queue)。

在每一次事件循环中,Dart总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。
首先,我们看看微任务队列。微任务顾名思义,表示一个短时间内就会完成的异步任务。从上面的流程图可以看到,微任务队列在事件循环中的优先级是最高的,只要队列中还有任务,就可以一直霸占着事件循环。
一般的异步任务通常也很少必须要在事件队列前完成,所以也不需要太高的优先级,因此我们通常很少会直接用到微任务队列

异步任务.jpg
2.1 Dart中的异步

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

Dart为Event Queue的任务建立提供了一层封装,叫作Future。(事件队列)

1) . Futrue : Dart 为 Event Queue 的任务建立提供了一层封装,叫作 Future

2). then: 的回调:拿到回调的结果

then 与 Future 函数体共用一个事件循环

这里给了我们两个实现异步逻辑的方法

官方:

2.2 异步函数

(减少嵌套, 异步代码同步写)
Dart中的异步操作主要使用Future以及async、await。
和kotlin里面的有返回值的协程很像!
对于异步函数返回的 Future 对象,如果调用者决定同步等待,则需要在调用处使用 await 关键字,并且在调用处的函数体使用 async 关键字
Dart 中的 await 并不是阻塞等待,而是异步等待。
和安卓一样, 我们平时也是单线程, 耗时操作的时候开线程, 这里不开线程,而是用关键字,让他异步执行!

协程:和kotlin一样, 用同步的代码写出异步的方式
Dart中的await并不是阻塞等待,而是异步等待

demo:

Future<List<Anchor>> getAnchors() async {
  // 1.读取json文件
  String jsonString = await rootBundle.loadString("assets/yz.json");

  // 2.转成List或Map类型
  final jsonResult = json.decode(jsonString);

  // 3.遍历List,并且转成Anchor对象放到另一个List中
  List<Anchor> anchors = new List();
  for (Map<String, dynamic> map in jsonResult) {
    anchors.add(Anchor.withMap(map));
  }
  return anchors;
}

3. Future的链式调用

then:
需要将一个大任务拆成很多小任务一步一步执行,需要使用Future.then函数来拆解任务

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');

4. 执行顺序和优先级

执行顺序

Future构造函数传入的函数体放在事件队列中
then的函数体要分成三种情况:

情况一:Future没有执行完成(有任务需要执行),那么then会直接被添加到Future的函数执行体后;
情况二:如果Future执行完后就then,该then的函数体被放到如微任务队列,当前Future执行完后执行微任务队列;
情况三:如果Future世链式调用,意味着then未执行完,下一个then不会执行
你只需要记住一点:then会在Future函数体执行完毕后立刻执行,无论是共用同一个事件循环还是进入下一个微任务。

如何创建微任务
在开发中,我们可以通过dart中async下的scheduleMicrotask来创建一个微任务:

import "dart:async";

main(List<String> args) {
  scheduleMicrotask(() {
    print("Hello Microtask");
  });
}

5. 多核异步任务Isolate, Isolate双向通信和 compute函数的使用

问题一: 如果在多核CPU中,单线程是不是就没有充分利用CPU呢?

Isolate: 相当于真正的多线程

尽管Dart是基于单线程模型的,但为了进一步利用多核CPU,将CPU密集型运算进行隔离,Dart也提供了多线程机制,即Isolate。在Isolate中,资源隔离做得非常好,每个Isolate都有自己的Event Loop与Queue,Isolate之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。

如果只有一个Isolate,那么意味着我们只能永远利用一个线程,这对于多核CPU来说,是一种资源的浪费。

举例: demo

loadData() async {
    // 通过spawn新建一个isolate,并绑定静态方法
    ReceivePort receivePort =ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);
    
    // 获取新isolate的监听port
    SendPort sendPort = await receivePort.first;
    // 调用sendReceive自定义方法
    List dataList = await sendReceive(sendPort, 'https://jsonplaceholder.typicode.com/posts');
    print('dataList $dataList');
}

// isolate的绑定方法
static dataLoader(SendPort sendPort) async{
    // 创建监听port,并将sendPort传给外界用来调用
    ReceivePort receivePort =ReceivePort();
    sendPort.send(receivePort.sendPort);
    
    // 监听外界调用
    await for (var msg in receivePort) {
      String requestURL =msg[0];
      SendPort callbackPort =msg[1];
    
      Client client = Client();
      Response response = await client.get(requestURL);
      List dataList = json.decode(response.body);
      // 回调返回值给调用者
      callbackPort.send(dataList);
    }    
}

// 创建自己的监听port,并且向新isolate发送消息
Future sendReceive(SendPort sendPort, String url) {
    ReceivePort receivePort =ReceivePort();
    sendPort.send([url, receivePort.sendPort]);
    // 接收到返回值,返回给调用者
    return receivePort.first;
}

Isolate双向通信

事实上双向通信的代码会比较麻烦;
Flutter提供了支持并发计算的compute函数,它内部封装了Isolate的创建和双向通信;
利用它我们可以充分利用多核心CPU,并且使用起来也非常简单
compute函数

在 Flutter 中,像这样执行并发计算任务我们可以采用更简单的方式。Flutter 提供了支持并发计算的 compute 函数,其内部对 Isolate 的创建和双向通信进行了封装抽象,屏蔽了很多底层细节,我们在调用时只需要传入函数入口和函数参数,就能够实现并发计算和消息通知。

Stream
Stream 也是用于接收异步事件数据,和 Future 不同的是,它可以接收多个异步操作的结果(成功或失败)。 也就是说,在执行异步任务时,可以通过多次触发成功或失败事件来传递结果数据或错误异常。 Stream 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。举个例子:

Stream.fromFutures([
  // 1秒后返回结果
  Future.delayed(Duration(seconds: 1), () {
    return "hello 1";
  }),
  // 抛出一个异常
  Future.delayed(Duration(seconds: 2),(){
    throw AssertionError("Error");
  }),
  // 3秒后返回结果
  Future.delayed(Duration(seconds: 3), () {
    return "hello 3";
  })
]).listen((data){
   print(data);
}, onError: (e){
   print(e.message);
},onDone: (){

});

上面的代码依次会输出:

I/flutter (17666): hello 1
I/flutter (17666): Error
I/flutter (17666): hello 3
总结:

在 Dart 中,并发是通过 Isolate 实现的

Dart是单线程的,但通过事件循环可以实现异步。而Future是异步任务的封装,借助于await与async,我们可以通过事件循环实现非阻塞的同步等待;Isolate是Dart中的多线程,可以实现并发,有自己的事件循环与Queue,独占资源。Isolate之间可以通过消息机制进行单向通信,这些传递的消息通过对方的事件循环驱动对方进行异步处理。

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

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

异步UI更新(FutureBuilder、StreamBuilder)
Flutter中有四个线程,dart, io ,原生main , gpu。我们的程序运行在dart线程上,异步处理属于伪协程和轮询的概念。默认异步就是插入但内置两个队列的micro task里等待执行,还是在dart中,除非你用isolate才会去io线程
Flutter 引擎线程模型

线程模型.jpg

Flutter Engine的主Task Runner,运行Platform Task Runner的线程可以理解为是主线程。类似于Android Main Thread或者iOS的Main Thread。

4大线程总结.jpg

6. 线程合并的问题。

我们在使用官方引擎的过程中,分别在独立多引擎和轻量级多引擎两个场景下的PlatformView时,都遇到了线程合并的问题。

线程合并.jpg
上一篇下一篇

猜你喜欢

热点阅读