zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Dart中的异步和多线程

2023-03-20 15:35:01 时间

首先,我们要明确,异步和多线程是两个概念,异步指的是不需要等待任务执行完毕就会接着执行接下来的任务,而多线程指的是多条线程一起执行任务。异步任务可以在单线程中执行,也可以在多线程中执行。

Dart中的异步编程

我们知道,Dart是一门单线程的语言,它不存在资源抢占的问题,因此Dart中的资源管理是非常简单方便的。

多线程肯定是比单线程要高效,这是毋庸置疑的,虽然Dart是一门单线程的语言,但是也无需担心其效率问题,因为在Dart中有多线程对应的解决方案,后面我们会讲到。

我们接下来所要聊的内容,也就是所谓的Dart中的异步编程,都是指的Dart单线程中的异步编程

首先来看一段代码:

String _data = "0";

void main() {
  _testMethod();
  print("执行其他的操作");
}

_testMethod() {
  print("开始");
  // 模拟耗时操作
  for (int i = 0; i < 1000000000; i++) {
    _data = "获取到的网络数据";
  }
  print("结束,_data=${_data}");
}

其打印结果如下:

flutter: 开始
flutter: 结束,_data=获取到的网络数据
flutter: 执行其他的操作

可以看到,代码是按照从上往下的顺序同步执行的。

在真正的开发过程中,遇到耗时操作,我们一般都是将其丢到异步里面去执行。那么在Dart中,如何异步执行某个任务呢?答案是使用Future。

接下来,我就将上例中的耗时操作放到异步里面去执行,如下:

String _data = "0";

void main() {
  _testMethod();
  print("执行其他的操作");
}

_testMethod() {
  print("开始");

  Future(() {
    // 模拟耗时操作
    for (int i = 0; i < 1000000000; i++) {
      _data = "获取到的网络数据";
    }
    print("结束,_data=${_data}");
  });
}

打印结果如下:

flutter: 开始
flutter: 执行其他的操作
flutter: 结束,_data=获取到的网络数据

可以看到,开始之后,没有等待耗时操作执行完毕,就执行了其他的操作,因为耗时的操作是异步执行的。

接下来我在_testMethod();和print("执行其他的操作");之间加一个sleep,让程序阻塞个5秒钟,如下:

通过打印结果可以看到,异步的操作是在最后执行的。也就是说,异步的操作会在外界同步操作执行完毕之后才会按照添加的顺序依次执行的

实际上,我们前面不是说了嘛,Dart是单线程,因此这里的异步指的是单线程中的异步,也就是说,是异步添加任务到单线程

接下来再对上述代码做一些调整,将异步操作中的print移到Future外面,如下:

可以看到,红框内的print没有等Future内的异步执行完毕就执行了

那么,如果我现在想要上例中的红框print代码等到Future中的异步代码执行完毕之后才执行,我要怎么办呢?答案是使用await,如下:

只有异步任务才可以使用await,await代表的是等待这个异步任务执行完毕,await所在的函数必须是异步(async)函数

上例中通过打印可以看到,结束的print是在Future任务执行完毕之后才执行的。

通过上面的例子我们知道了,Future可以用来执行异步操作,接下来我们就来详细看一下Future的使用。

Future的详细使用

首先来看个例子:

可以看到,Future类型的实例对象future是有一个then函数的,在then函数中获取到的value值是chuan构建Future实例对象的时候的回调参数里面返回的值,也就是说,放进Future里面的异步操作在执行完毕之后会返回一个值,该值可以在then函数中获取到

接下来我们想一个问题,很多的异步操作比如说网络请求,是会抛出异常的,这个异常信息我们可以通过catchError函数来获取到

可以看到,异步任务中抛出的异常,在catchError中被获取到了

但是上例中有一个问题,就是我已经通过catchError处理了错误异常了,但是终端里面还是抛出了异常,然后程序还是报错了,这是为啥呢?

下面我将then相关的内容注释掉,再看一下:

此时就会捕获到异常信息,并且程序不会报错了。

我们不禁要想,为啥捏?其实,这正是Dart作为一门现代化的高级语言的一种魅力体现。Dart的设计者不希望你将then和catchError分开处理,因此,我接下来将then和catchError写在一起

通过执行结果我们看到,将then和catchError放在一起处理,在抛出异常的时候会捕获到该异常并且程序不会报错,在没抛出异常的时候就会在then里面正常处理异步的结果

另外还需要说明的一点是,Future实例的所有的方法返回的都是Future实例自身,目的就是可以让你链式调用。实际上,我自己在项目中封装的链式调用工具也是采用的该思想。

除了使用catchError的方式来捕获异常之外,在then函数中还有一个隐藏参数onError也可以捕获异常

建议是使用then函数中隐藏参数onError的方式来捕获异常。因为如果采用catchError的方式捕获异常的话,catchError和then的先后顺序会影响then里面内容的执行:如果catchError在前,then在后,那么捕获到异常之后,then里面的内容还会执行;如果then在前,catchError在后,那么捕获到异常之后,then里面的内容就不会继续执行了

我们也可以通过whenComplete函数来监听异步任务的处理完成状态,如下:

需要注意的是,无论是否抛出异常,whenComplete都会执行。

添加多个异步任务

在日常的开发过程中,肯定会遇到很多同时添加多个异步任务的场景,接下来我们就来详细探讨一下。

首先看下面这个例子:

可以看到,异步任务是在其他任务执行完毕之后才会执行的,每一个异步任务都是按照添加的顺序依次执行的,并且一个异步任务执行完毕之后才会继续执行下一个异步任务

通过上面的例子我们知道,异步任务是可以按照被添加的顺序依次执行的,但是在真实的项目开发过程中,如果我们想要控制异步任务的执行顺序,我们肯定不能通过上述的这种异步任务添加的方式依次添加的,因为这样做的话,会导致异步任务的添加分布在项目的各个角落,代码的可读性将会非常差,而且后期维护成本会很高。

那么,我们如何统一地去控制异步任务的添加执行顺序呢?答案是使用then,示例如下:

执行结果如下:

关于上例,有如下几点需要说明:

1,Future中的任何函数都会返回该Future对象,then函数也不例外。

2,Future中的异步任务执行完毕之后,我们可以通过then函数来接收异步任务执行完毕之后返回的结果。

3,Future的then函数可以多次连环调用,上一个then函数中的任务执行完毕之后,可以通过return来返回执行的结果,并且可以通过下一个then函数来接收上一个then中return的结果

4,下一个then中的内容一定是在上一个then中的内容执行完毕之后才开始执行的

5,如果我们想统一控制异步任务的执行顺序,那么就可以通过在一个Future中多次连环调用then的方式来实现。

6,一个Future中通过多个then添加的多个任务,是同一个异步级别,也就是说,该Future中的所有任务可以理解成是一个大的异步任务,这一个大的异步任务里面又有很多小的子任务,这些小的子任务(then中的任务)的执行是有先后顺序的,但是这些小的子任务中间不会穿插其他的Future的任务。也就是说,一个Future中的任务(包括通过多个then添加的多个子任务)一旦执行,那么就会待该Future中的所有任务都执行完毕之后才会执行其他的Future中的任务

也许你会有疑问,这种情况下如何捕获异常呢?答案是,在最后通过catchError来捕获异常:

打印如下:

需要注意的是,catchError一定要放到最后统一处理,如果将其放到中间的话,那么在捕获到异常之后,catchError后面的内容还是会执行的

下面再来考虑一个问题,上面我演示的是,多个异步任务的结果在每个异步任务执行完之后分别进行处理的场景;但是有些业务场景是这样的,多个异步任务都执行完毕之后,再去统一处理各个异步任务的结果,这时应该怎么办呢?

此时应该使用Future的wait函数,如下:

说明如下:

1,Future的wait函数可以以数组的形式包裹多个Future异步任务

2,wait函数后面紧挨着的then函数可以接收wait里面包裹的多个异步任务的结果,这些结果被封装进数组里面。

Dart的事件循环(event loop)

上面我们提到,一个Future中的then里面的任务会紧跟着该Future的异步任务执行完毕之后执行,并不会在中间穿插其他的Future中的任务,其背后的原理是啥呢?接下来咱就一探究竟。

在Dart当中,实际上是有两种队列的:

  1. 事件队列(event queue),包含所有的外来事件,比如I/O、drawing events、timers、isolate之间的信息传递等。
  2. 微任务队列(microtask queue),表示一个短时间内就能完成的异步任务。microtask queue的优先级是最高的,高于event queue,只要微任务队列中还有任务,其就可以一直霸占着事件循环。

microtask event添加的任务主要由Dart内部产生,我们程序员极少会使用微任务队列。

由于microtask queue的优先级高于event queue,所以如果微任务队列里面有太多的任务,那么就有可能会霸占住当前的事件队列,从而对event queue中的触摸、绘制等外部事件造成阻塞、卡顿。

Dart中的事件循环图示如下:

1,首先会执行主任务。

2,待所有的主任务依次执行完毕之后,会检查微任务队列里面有没有任务。如果微任务队列里面有任务,那么就取出排在最前面的任务开始执行;执行完毕之后再次检查微任务队列里面是否还有任务,重复上述动作,知道微任务队列为空为止。

3,如果微任务队列为空,那么就检查事件队列是否为空。如果事件队列里面有任务,那么就取出排在最前面的任务执行;执行完毕之后,会首先检查微任务,具体流程可参见上述步骤2,微任务队列处理完成之后再检查事件队列是否为空,如果不为空则取出第一个任务执行,如此循环往复,直至事件队列为空为止。

前面我们说了,作为开发者,我们一般是不使用异步任务中的microtask queue的,我们使用的最多的是优先级更低的event queue。Dart为event queue中的任务创建提供了一层封装,就是我们已经很熟悉的Future

接下来我们来看个例子:

下面来分析一下上例:

1,首先依次执行如下主任务

  1. 打印“开始添加异步任务”
  2. 将异步任务1添加到event queue
  3. 将异步任务2添加到event queue
  4. 将微任务1t添加到microtask queue
  5. 打印“结束添加异步任务”
  6. 打印“执行其他的操作”

2,主任务执行完毕之后,执行微任务1

3,微任务1执行完毕之后,微任务队列里面就空了,开始执行event queue里的第一个任务,即异步任务1

4,异步任务1执行期间,会将该Future的所有then里面的任务依次放到microtask queue中,所以在异步任务1执行完毕之后,会监测到microtask queue中有微任务,因此会依次执行微任务2和微任务3

5,微任务2和微任务3执行完毕之后,微任务队列就空了,因此会从event queue里面取出异步任务2执行。

接下来再来看个例子:

void main() {
  _testMethod();
}

_testMethod() {
  debugPrint("1");

  Future future1 = Future(() => null);
  future1.then((value) {
    debugPrint("2");
    scheduleMicrotask(() => debugPrint("3"));
  }).then((value) => debugPrint("4"));

  Future future2 = Future(() => debugPrint("5"));

  Future(() => debugPrint("6"));

  scheduleMicrotask(() => debugPrint("7"));

  future2.then((value) => debugPrint("8"));

  debugPrint("9");
}

运行结果如下:

flutter: 1
flutter: 9
flutter: 7
flutter: 2
flutter: 4
flutter: 3
flutter: 5
flutter: 8
flutter: 6

分析如下:

1,主任务按如下次序依次执行

  1. 打印任务1
  2. 打印任务9
  3. 添加future1异步任务到event queue
  4. 添加future2异步任务到event queue
  5. 添加异步打印任务6到event queue
  6. 添加异步打印任务7到microtask queue

2,主任务执行完毕之后,监测到微任务队列非空,因此执行打印任务7

3,微任务队列空了之后,从event queue取出future1异步任务开始执行,与此同时将其相关连的的两个then中的任务依次添加进microtask queue中

4,future1异步任务执行完毕之后,监测到微任务队列里面有两个任务,所以依次执行如下任务:打印任务2、添加打印任务3到微任务队列,打印任务4,打印任务3

5,现在微任务队列没有任何任务了,所以从Event queue中取出future2异步任务,与此同时,将future2对应的then中的任务加入微任务队列中,所以依次执行打印任务5、打印任务8

6,现在微任务队列没有任何任务了,所以从Event queue中取出打印任务6开始执行。

接下来我们再对上例做一个拓展,如下:

分析如下:

1,主任务按如下次序依次执行

  1. 打印任务1
  2. 添加future1异步任务到event queue
  3. 添加future2异步任务到event queue
  4. 添加异步打印任务6到event queue
  5. 添加异步打印任务7到microtask queue
  6. 打印任务9

2,主任务执行完毕之后,监测到微任务队列非空,因此执行打印任务7

3,微任务队列空了之后,从event queue取出future1异步任务开始执行,与此同时将其相关连的的两个then中的任务依次添加进microtask queue中

4,future1异步任务执行完毕之后,监测到微任务队列里面有两个任务,所以依次执行如下任务:打印任务2、添加打印任务3到微任务队列,打印任务4,打印任务3

5,现在微任务队列没有任何任务了,所以从Event queue中取出future2异步任务,与此同时,将future2对应的两个then中的任务依次加入微任务队列中,所以先执行event queue中的打印任务5,然后执行mcrotask queue中的打印任务8、添加打印任务10到event queue、打印任务11

6,现在微任务队列没有任何任务了,所以从Event queue中取出打印任务6开始执行。

7,打印任务6执行完毕之后,微任务队列仍旧没有任务,因此继续从event queue中取出打印任务10并执行。

Dart中的多线程

首先要声明一下,Dart是一门单线程的语言,它没有像OC、Swift那样复杂的多线程控制。也可以这样理解,Dart只有一个主线程,没有其他的线程

我们这里讲的Dart中的多线程,实际上指的是如何在Dart中去实现类似于多线程的效果,并不是真的多线程。

在Dart中,可以通过Isolate或者compute来实现多线程。

Isolate

先来看个例子:

_testMethod() {
  debugPrint("1");

  Isolate.spawn(testFunc, "多线程1");

  sleep(Duration(seconds: 2));

  debugPrint("2");
}

上面代码的执行顺序如下:

可以看到,通过Isolate添加进去的任务,没有等主线程中的任务执行完毕就执行了

如果按照之前的异步任务的思路去理解的话,多线程1的打印应该在最后执行,而这里显然没有。其实这很容易解释,通过Future是往主线程异步添加任务,所以各个任务是同步排队执行;而通过Isolate添加的任务,是在另外一条线程中异步执行的。

好,现在我们知道了,在Dart中可以通过Isolatela实现多线程。但是实际上,Isolate更像是进程而非线程,因为Isolate拥有独立的内存空间,并且Isolate之间的通信需要借助到端口(port)概念的api,这些特性让它看起来更像进程。

Isolate的中文翻译是隔离,也就是说,两个Isolate是完全隔离的。下面来看个例子:

可以看到,在Isolate中将a值修改为100之后,在主线程中再获取,a值仍旧是0,并未改为100。这是为啥呢?原因就是, 多个Isolate之间的数据是隔离的,在另外一个isolatel里面操作数据,相当于在另外一个进程里面去操作数据,这个操作只会影响当前进程内部,不会影响到其他的进程

Isolate的数据隔离的一大优势就是,程序员不需要担心多线程之间的资源抢占问题,无需通过加锁等复杂操作来保证程序的正确运行。但是Isolate的数据隔离也导致了一个问题,那就是如何保证多个Isolate之间的数据同步呢?答案就是使用端口

代码如下:

说明如下:

1,可以看到,通过ReceivePort就可以实现不同Isolate之间数据的传递

2,port的意思就是端口,所谓端口,指的就是不同设备或者不同进程之间通信所用。这也是为什么我说Isolate更像是一个进程而非线程。

3,在主Isolate中通过端口接收到数据并处理完成后,要将对应的端口和isolate都给清理掉。

4,这里的testFunc函数中的内容是在多线程中执行的_receivePort.listen代码块中的内容是在主线程中执行的,需要等主线程中之前添加的其他任务执行完毕之后才会执行到这里

通过上例可以看到,Isolate的操作是非常底层的,使用起来相对而言会比较麻烦。Dart官方为了方便开发者使用多线程,还提供了compute接口。

compute

compute是对Isolate的更高级的封装,相对于Isolate而言,compute的使用更简单更轻量。

可以看到,compute的api比Isolate要更简单。

这里的testFunc中的内容是在多线程中执行的;而compute.then里面的内容是在主线程中执行的,需要等主线程中之前添加的任务执行完毕之后才会执行到这里

compute可以直接通过return来将计算后的值返回到外层,然后通过compute.then来获取到;Isolate要通过Port才能将计算后的值传递到另外一个Isolate中去。但是实际上,compute在最底层也是通过Isolate和port来实现的数据传递,只不过compute对之进行了更高级的封装,然后提供给程序员更为便捷的接口罢了。

总结

在Flutter开发中,我们绝大部分情况下是使用Future异步来实现各种需求场景;只有那些非常复杂耗时的计算我们才回去考虑使用多线程,方式就是Isolate或者compute。

尽管我们可以使用Isolate或者compute来实现类似于多线程的功能,但是它更像进程而非线程,因此Isolate的使用是很占内存的,所以非必要不使用。

以上。