我们在之前的文章中已经讲解了怎么使用FuturescheduleMicrotask,但是我们发现,这两个都是同步的,其执行顺序都是可以确定的。接下来我们介绍Dart中的异步多线程操作;

Isolate

我们先来看一段代码执行的结果:

  1. void testIsolate() {
  2. print('1');
  3. Future(() => print('3'));
  4. sleep(const Duration(seconds: 2));
  5. print('2');
  6. }

我们根据之前的学习,可以知道打印结果是1、2、3,我们看一下打印结果:
iShot2021-11-14 19.06.19.gif
根据打印结果,也确实验证了我们的结论;但是很明显,即使我们阻塞了主线程,Future里边的任务依然没有先执行;那么有没有办法能够在主线程阻塞的时候,让3先打印出来呢?这个时候就需要用到Flutter中的多线程操作Isolate了;

我们将代码修改如下:

  1. void testIsolate() {
  2. print('1');
  3. Isolate.spawn(func, 10);
  4. sleep(const Duration(seconds: 2));
  5. print('2');
  6. }
  7. func (int count) => print('3');

此时,我们来看一下打印结果:
iShot2021-11-14 19.10.51.gif
可以看到,我们阻塞了主线程,即使2没有第一时间打印出来,但是不影响3的输出,也就是Isolate没有因为主线程的阻塞,而延后调用;那么Isolate是否实在子线程呢?

为了验证Isolate是否在子线程,我们将代码修改如下:

  1. void testIsolate() {
  2. print('1');
  3. Isolate.spawn(func, 10);
  4. Isolate.spawn(func2, 10);
  5. Isolate.spawn(func, 10);
  6. Isolate.spawn(func2, 10);
  7. Isolate.spawn(func, 10);
  8. Isolate.spawn(func2, 10);
  9. sleep(const Duration(seconds: 2));
  10. print('2');
  11. }
  12. func (int count) {
  13. print('第一个');
  14. }
  15. func2 (int count) {
  16. print('第二个');
  17. }

执行结果:
iShot2021-11-14 19.20.00.gif
可以看到打印顺序不再是固定的了,变成了随机的,证明了Isolate确实是在子线程;

Isolate的独立内存空间

需要注意的是,Dart中的多线程不仅仅是开辟了一条’线程‘,此处的Isolate与其说是线程,不如说他更像一个进程,因为Isolate有独立的内存空间(主要是自己创建的对象/数据);这就意味着,每一个Isolate之间的数据是独立的,不会存在资源抢夺的问题,也就不需要使用进行操作,那么我们访问数据的时候,也就不能直接访问;

我们来看一段代码:

  1. void testIsolate() {
  2. print('1');
  3. Isolate.spawn(func, 100);
  4. sleep(const Duration(seconds: 2));
  5. print('a = $a');
  6. print('2');
  7. }
  8. int a = 10;
  9. func (int count) {
  10. a = count;
  11. print('func 中 a: $a');
  12. }

在这段代码中,我们定义了一个变量a,默认值为10,然后在func方法中奖count值赋值给a,然后在Isolate中,我们将给func方法传值为100,那么按照正常的逻辑,我们在主线程阻塞2秒之后的打印结果应该是a = 100,那么是不是这样呢?我们来看一下打印结果:
iShot2021-11-14 19.33.46.gif
我们看到,即使我们在func方法中,给a赋值为100,但是在之后的打印中a还是10,这也就意味着Isolate中的资源是不能被抢夺的,它是独立的;

获取Isolate中被修改的数据

但是,我们确实在某些时候,需要在子线程中去修改a的值,那么我们应该如何操作呢?我们将代码修改如下:

  1. void testIsolate() {
  2. print('1');
  3. // 创建一个接口port
  4. ReceivePort port = ReceivePort();
  5. // 创建一个Isolate
  6. Isolate.spawn(func, port.sendPort);
  7. // 通过port监听数据的变化
  8. port.listen((message) {
  9. a = message;
  10. print('port中监听到 a = $a');
  11. });
  12. sleep(const Duration(seconds: 2));
  13. print('a = $a');
  14. print('2');
  15. }
  16. int a = 10;
  17. func (SendPort sendPort) {
  18. sendPort.send(100);
  19. print('func 中 a: $a');
  20. }

此时,我们再次来查看运行结果:
iShot2021-11-14 19.42.59.gif
我们看到,此时我们可以监听到a被修改了;

在这个过程中,我们做了四件事:

  • 创建一个接口:ReceivePort;
  • 创建一个Isolate;
  • 使用创建的ReceivePort来监听数据的变化;
  • Isolate关联的方法中使用SendPort发送消息进行传值;

通过以上四步操作,我们可以修改Isolate内部的数据,并获取到新值;

但是需要注意的是,我们使用这种方式开监听数据的时候,相当于开辟了空间,那么就需要我们自己来管理销毁;最终代码如下:

  1. void testIsolate() async {
  2. print('1');
  3. // 创建一个接口port
  4. ReceivePort port = ReceivePort();
  5. // 创建一个isolate
  6. Isolate isolate = await Isolate.spawn(func, port.sendPort);
  7. // 通过port监听数据的变化
  8. port.listen((message) {
  9. a = message;
  10. print('port中监听到 a = $a');
  11. // 关闭port
  12. port.close();
  13. // 销毁 isolate
  14. isolate.kill();
  15. });
  16. sleep(const Duration(seconds: 2));
  17. print('a = $a');
  18. print('2');
  19. }
  20. int a = 10;
  21. func (SendPort sendPort) {
  22. sendPort.send(100);
  23. print('func 中 a: $a');
  24. }

我们需要将ReceivePort关闭掉,将Isolate进行销毁;

需要注意的是,Isolate是多线程操作,所以此处的await并不会让后续代码产生类似Future的等待效果;

compute

Flutter中的多线程除了Isolate之外,还有一个compute,它是对Isolate的封装;我们来看一段代码:
image.png
我们来看这段代码的执行效果:
iShot2021-11-14 20.03.41.gif
从执行效果我们可以确定,compute也是在子线程执行的;

但是computeIsolate是有区别的,compute可以接收任务func中的返回值的;我们将代码修改如下:
image.png
查看运行结果:
iShot2021-11-14 20.08.03.gif
可以看到我们接收到了func方法的返回值;

需要注意的是,computeawait会引起和Future中同样的等待效果;

那么compute是否是数据隔离呢?

我们对代码进行如下修改:
image.png
运行结果:
iShot2021-11-14 20.16.56.gif
可以看到compute可以接收任务的返回数据,但是在任务内部针对全局变量a的修改,在外部依然无法获取;