Flutter-Dart中的异步和多线程讲解

3,525 阅读8分钟

众所周知,Dart是一门单线程的语言,我们可以将一些耗时的任务放到异步操作中,但是异步任务必须等线程空闲时才会去执行,这是无法满足有些场景需求的,下面就来讲下如何处理这些场景。

如何处理耗时的操作

不同语言的不同处理方式

  • 多线程。比如 Java、C++,就是开启一个新的线程,将耗时操作放在新的线程里面处理,再通过线程间通信的方式,将拿到的数据传给主线程处理。
  • 单线程+事件循环。比如 JavaScript、Dart 都是基于单线程加事件循环来完成耗时操作的处理。

单线程的异步操作

应用程序大部分时间是处于空闲状态的,并不是一直在和用户进行交互。而我们的操作系统存在阻塞式调用非阻塞式调用

  • 阻塞式调用:调用结果返回之前,当前线程会被挂起,调用线程只有在得到调用结果之后才会继续执行。
  • 非阻塞式调用:调用执行之后,当前线程不会停止执行,只需要间隔一段时间来检查一下有没有结果返回即可。

Dart 的异步操作就是利用非阻塞式调用实现的。

什么是事件循环

和 iOS 应用很像,在 Dart 的线程中也存在事件循环和消息队列的概念,但在 Dart 中线程叫做isolate。应用程序启动后,开始执行 main 函数并运行 main isolate

每个 isolate 包含一个事件循环以及两个事件队列,event loop事件循环,以及event queuemicrotask queue事件队列,event 和 microtask 队列有点类似 iOS 的 source0 和source1。

  • event queue:负责处理I/O事件、绘制事件、手势事件、接收其他 isolate 消息等外部事件。

  • microtask queue:可以自己向 isolate 内部添加事件,事件的优先级比 event queue高。

Dart 中的异步

Dart中的异步操作主要使用Future以及asyncawait,async 和 await 是要一起使用的,这就是协程的一个语法糖。

  • Future 延时操作的一个封装,可以将异步任务封装为Future对象,我们通常通过then()来处理返回的结果
  • async 用于标明函数是一个异步函数,其返回值类型是Future类型
  • await 用来等待耗时操作的返回结果,这个操作会阻塞到后面的任务

什么是协程

协程分为无线协程有线协程,无线协程在离开当前调用位置时,会将当前变量放在堆区,当再次回到当前位置时,还会继续从堆区中获取到变量。所以,一般在执行当前函数时就会将变量直接分配到堆区,而asyncawait就属于无线协程的一种。有线协程则会将变量继续保存在栈区,在回到指针指向的离开位置时,会继续从栈中取出调用。

async、await原理

以 async、await为例,协程在执行时,执行到async则表示进入一个协程,会同步执行async的代码块。async的代码块本质上也相当于一个函数,并且有自己的上下文环境。当执行到await时,则表示有任务需要等待,CPU 则去调度执行其他 IO,也就是后面的代码或其他协程代码。过一段时间 CPU 就会轮循一次,看某个协程是否任务已经处理完成,有返回结果可以被继续执行,如果可以被继续执行的话,则会沿着上次离开时指针指向的位置继续执行,也就是await标志的位置。

由于并没有开启新的线程,只是进行 IO 中断改变 CPU 调度,所以网络请求这样的异步操作可以使用asyncawait,但如果是执行大量耗时同步操作的话,应该使用isolate开辟新的线程去执行。

下面举例来讲解异步

  • 模拟一个同步的耗时操作,看会输出怎样的结果

截屏2021-12-06 下午9.30.18.png

输出结果,C 并没有因为有耗时操作而影响线程的任务执行

flutter: B
flutter: C
flutter: A
flutter: D
  • 那现在对这个例子改造一下,加上 async、await

截屏2021-12-06 下午9.37.14.png

输出结果是,C 等待了耗时操作完成之后才执行。使用 async 来标明 getData 这个函数是一个异步函数,await 用于等待请求返回的结果,此时会阻塞掉后面的代码,只有当请求结束后面的代码才会执行。

flutter: B
flutter: A
flutter: D
flutter: C
  • 多Future 情况下执行顺序是什么样的

截屏2021-12-06 下午9.42.29.png

执行的顺序是按着创建顺序执行

flutter: A
flutter: B
flutter: C
flutter: D
  • Future 是链式调用的,可以在后面调用
//处理返回结果
.then((value) => null)
//处理错误
.onError((error, stackTrace) => null)
//完成回调
.whenComplete(() => null)
//处理异常
.catchError(onError);

Dart 中的事件循环

  • 微任务队列:表示一个短时间内就会完成的异步任务,它的优先级比事件队列高。

  • 事件队列:包含所有的外来事件,比如:I/O、手势、绘图等。

这是一张 Flutter 任务队列的执行图:

截屏2021-12-07 下午9.20.41.png

这两个队列也是有优先级的,当 isolate 开始执行后,会先处理 microtask 的事件,当microtask 队列中没有事件后,才会处理 event队列中的事件,并按照这个顺序反复执行。但需要注意的是,当执行 microtask 事件时,会阻塞 event 队列的事件执行,这样就会导致渲染、手势响应等 event 事件响应延时。为了保证渲染和手势响应,应该尽量将耗时操作放在 event 队列中。

下面这个例子可以证明这一点:

截屏2021-12-06 下午9.59.19.png

flutter: 开始执行
flutter: 结束执行
flutter: 微任务
flutter: A
flutter: A结束
flutter: B
flutter: B结束

假如微任务添加在异步任务里面,异步任务和微任务谁先执行呢?看下面这个例子:

截屏2021-12-07 下午9.28.31.png

执行结果是异步任务里面的微任务没有异步任务先执行,并且异步任务链式调用的处理也比微任务优先

flutter: 开始执行
flutter: 结束执行
flutter: 微任务
flutter: A
flutter: A结束
flutter: A里面的微任务

多线程

在一个页面中做耗时比较大的运算时,就算用了 async、await 异步处理,UI页面的动画还是会卡顿,因为还是在这个UI线程中做运算,异步只是你可以先做其他,等我这边有结果再返回,但是,我们的计算仍旧是在这个UI线程,仍会阻塞UI的刷新,异步只是在同一个线程的并发操作。所以这个时候就需要创建新的线程来执行耗时操作解决这个问题。

什么是 Isolate

Isolate 是 Dart 平台对线程的实现方案,但和普通 Thread 不同的是,isolate 拥有独立的内存,isolate 由线程和独立内存构成。正是由于 isolate 线程之间的内存不共享,所以 isolate 线程之间并不存在资源抢夺的问题,所以也不需要锁。通过 isolate 可以很好的利用多核 CPU,来进行大量耗时任务的处理。

但是12月28号,Google发布了Dart2.15版本。我们首先重新设计和实现了 isolate 的工作方式,引入了一个新概念: isolate 组。Isolate 组中的 isolate 共享各种内部数据结构,这些数据结构则表示正在运行的程序。这使得组中的单个 isolate 变得更加轻便。如今,因为不需要初始化程序结构,在现有 isolate 组中启动额外的 isolate 比之前快 100 多倍,并且产生的 isolate 所消耗的内存减少了 10 至 100 倍。关于Dart2.15版本更多的内容可以参考:mp.weixin.qq.com/s/g-1uCl3up…

先看下面这个异步的例子,一看就知道执行顺序是 A->B->C

截屏2021-12-06 下午10.13.03.png

下面举例说明 Dart 中确认存在多线程

截屏2021-12-06 下午10.39.54.png

执行结果是这样的,可以看出确实没有按着创建的顺序执行

flutter: A
flutter: 第二个
flutter: 第二个
flutter: 第一个
flutter: 第二个
flutter: 第一个
flutter: 第二个
flutter: 第一个
flutter: 第一个
flutter: B

Isolate 通信机制

isolate 线程之间的通信主要通过 port 来进行,这个 port 消息传递的过程是异步的。通过建立通信双方的 sendPortreceiveport,进行相互的消息传递。

截屏2021-12-06 下午10.41.50.png

这样就实现了通信。isolate实现方法需要用 static 修饰,不然会报下面这个错误

Unhandled Exception: Invalid argument(s): Isolate.spawn expects to be passed a static or top-level function

什么是compute

dart 中的 Isolate 比较重量级,UI 线程和 Isolate 中的数据的传输比较复杂,因此 Flutter 为了简化用户代码,在 Foundation 库中封装了一个轻量级 compute 操作来实现。这个使用非常方便,并且可以直接返回值。

import 'package:flutter/foundation.dart';

void computeTest() async {
  print('开始执行');
  int b = await compute(test1, 10);
  print('结束执行: b = $b');
}

static int test1(int count) {
  sleep(Duration(seconds: 2));
  print('执行方法');
  return 100;
}

使用场景

  • 任务执行事件很短的,比如几十毫秒以内的建议用 Future
  • 任务执行时间长,只有一次返回的用compute,有多次返回的用Isolate

Mac Flutter环境配置及Android Studio的使用

Flutter-最全常用快捷键

参考资料:www.jianshu.com/p/54da18ed1…

欢迎关注、点赞及转发。