Flutter Dart之异步操作(Future、scheduleMicrotask)、多线程(Isolate和compute)

1,898 阅读6分钟

和谐学习!不急不躁!!我是你们的老朋友小青龙~

前言

Dart是单线程语言,它有两种队列:事件队列、微任务队列。

  • 事件队列:常见的情况就是绘制事件、鼠标事件、文件流读写事件、计时、Dart isolate之间的消息通讯等所有外来事件。

  • 微任务队列:表示需要短时间内快速处理的异步任务,它的优先级高于事件队列。

什么是异步任务?

假设有任务A、任务B、任务C,任务B需要消耗很长一段时间才能完成。
但是任务B的执行不会卡住任务C的执行,我们称任务B是`异步任务`

异步任务有哪些方式?

  • Future

  • scheduleMicrotask

正文

Future

在开始正文内容之前,我们先了解一个API - - - Future

关于Future函数,文档的解释如下:

/// Creates a future containing the result of calling [computation]
/// asynchronously with [Timer.run].
百度翻译:创建包含调用[computation(计算)]结果的future(未来),与[Timer.run]异步

所以,Future函数包含的代码块是「异步执行」的。

1、非异步任务

假设我们有任务A、任务B(耗时任务)、任务C,正常情况下是这样的:

void main() {
  print('任务 A');
  getData();
  print('任务 C');
}

getData() {
  print('任务 B');
  for (int i = 0; i < 10; i++) {
    print(i);
  }
  print('for循环打印结束');
}

运行效果:

image.png

任务B会堵塞当前任务,依次执行任务A、任务B、任务C。

2.1、异步任务

需求:假如我希望任务B不堵塞当前线程。

方案:把耗时任务放到Future函数里即可。

void main() {
  print('任务 A');
  getData();
  print('任务 C');
}

getData() {
  Future(() {
    print('任务 B');
    for (int i = 0; i < 10; i++) {
      print(i);
    }
    print('for循环打印结束');
  });
}

运行效果:

image.png

当然异步除了「Future」还可以用「scheduleMicrotask」实现,这个后面会讲到。

2.2、异步任务之 async + await Future

需求:假如我希望print('for循环打印结束');这句代码写到Future外面呢?

方案:可以利用 async + await Future组合来实现。

getData() async {
  await Future(() {
    print('任务 B,async + await Future');
    for (int i = 0; i < 10; i++) {
      print(i);
    }
  });
  print('for循环打印结束');
}

运行效果:

image.png

注意:

  • await不能单独使用,必须结合async来使用。(即await必须在异步函数里使用)

  • 注意:await属于任务阻塞,不是线程阻塞

异步任务 - 使用场景

需求:需要查询个人账户变动信息

要调用查询个人账户变动信息接口,其中一个很关键的参数是身份识别码,而身份识别码是在个人信息接口里才返回的。

所以getData函数里包含两个异步请求:

  • 个人信息接口

  • 个人账户变动信息

实现方式一:async + await Future

getData() async {
  await Future(() {
    print('任务 B,async + await Future');
    for (int i = 0; i < 10; i++) {
      print(i);
    }
    print('个人信息接口结束');
  });

  for (int i = 100; i < 110; i++) {
    print(i);
  }
  print('个人账户变动信息结束');
}

运行效果:

image.png

实现方式二:每个请求分别用Future包裹

getData() {
  Future(() {
    print('任务 B,每个请求分别用Future包裹');
    for (int i = 0; i < 10; i++) {
      print(i);
    }
    print('个人信息接口结束');
  });

  Future(() {
    for (int i = 100; i < 110; i++) {
      print(i);
    }
    print('个人账户变动信息结束');
  });
}

运行效果:

image.png

实现方式三:Future点语法then

getData() {
  Future(() {
    print('任务 B,Future点语法then');
    for (int i = 0; i < 10; i++) {
      print(i);
    }
    print('个人信息接口结束');
    return 'xdxxd13567';
  }).then((value) {
    print('身份识别码是:$value');
    for (int i = 100; i < 110; i++) {
      print(i);
    }
    print('个人账户变动信息结束');
  });
}

运行效果:

image.png

Future点语法then的多样玩法

查看Future函数内部设计

「command + 单机」 进入Future文档页面,可以看到Future内部是这样设计的:

factory Future(FutureOr<T> computation()) {
  _Future<T> result = new _Future<T>();
  Timer.run(() {
    try {
      result._complete(computation());
    } catch (e, s) {
      _completeWithErrorCallback(result, e, s);
    }
  });
  return result;
}
  • 首先它是一个工厂(factory)方法
  • 参数是一个回调方法
  • 返回值result是Future类型
查看then函数内部设计

then函数的内部设计如下:

Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});
  • then同样也是返回一个Future类型
怎么玩

利用Future函数和then函数都会返回Future的这个特性,我们可以实现任务叠加的功能。即:

/// Future点语法then的多样玩法
getData() {
  Future _f = Future(() {
    print('任务 B,Future点语法then的多样玩法');
    for (int i = 0; i < 10; i++) {
      print(i);
    }
    print('个人信息接口结束');
    return 'xdxxd13567';
  }).then((value) {
    print('身份识别码是:$value');
    for (int i = 100; i < 110; i++) {
      print(i);
    }
    print('个人账户变动信息结束');
  });

  _f.then((value) {
    print('任务D 开始执行');
  }).then((value) {
    print('任务F 开始执行');
  });

  _f.then((value) {
    print('任务I 开始执行');
  });
}

可以在原来的then基础上继续.then,也可以单独定义Future对象来接收前面任务结束的结果。

实现效果:

image.png

多个异步任务,其中一个优先执行(scheduleMicrotask)

scheduleMicrotask

需求:假如我有多个异步下载任务,其中一个任务需要优先执行。

方案:scheduleMicrotask

/// 任务优先执行
getData() {
  print('getData 开始执行');
  Future(() {
    print('任务 1 开始执行');
    return '任务1';
  }).then((value) {
    sleep(const Duration(seconds: 2));
    print('任务 2 开始执行');
    return '任务2';
  });
  print('任务 C');
  sleep(const Duration(seconds: 2));
  scheduleMicrotask(() {
    print('任务 X');
  });
}

运行效果:

image.png

多个scheduleMicrotask,又是如何运行的呢?

/// 任务优先执行
getData() {
  print('getData 开始执行');
  Future(() {
    print('任务 1 开始执行');
    return '任务1';
  }).then((value) {
    sleep(const Duration(seconds: 2));
    print('任务 2 开始执行');
    return '任务2';
  });
  print('任务 C');
  sleep(const Duration(seconds: 2));
  scheduleMicrotask(() {
    print('任务 X');
  });

  scheduleMicrotask(() {
    print('任务 Y');
  });

  scheduleMicrotask(() {
    print('任务 Q');
  });
}

经测试,XYQ任务执行顺序:任务 X -> 任务 Y -> 任务 Q

即:多个scheduleMicrotask,是同步执行的。

Future.then里包含scheduleMicrotask

/// Future.then 里包含scheduleMicrotask
getData() {
  print('getData 开始执行');
  Future(() {
    print('任务 1');
  }).then((value) {
    sleep(const Duration(seconds: 2));
    // 这里添加一个scheduleMicrotask任务
    scheduleMicrotask(() {
      print('任务2');
    });
    print('任务3');
  }).then((value) {
    print('任务4');
  });

  print('任务5');
  sleep(const Duration(seconds: 2));
  scheduleMicrotask(() {
    print('任务6');
  });
}

小伙伴们,你们猜猜看打印顺序会是什么呢?

运行效果:

image.png

打印任务2的scheduleMicrotask居然是最后执行的,这是为什么呢?

原因是:Dart属于单线程语言,当它在执行Dart的函数时候,不会被Dart的其它函数所影响所打断。

首先,scheduleMicrotask属于Microtask(微任务),它优先级高于Future任务,所以「任务6」比「任务1」优先打印;

其次,从代码上来看:「任务2」对应的scheduleMicrotask任务是在Future的then里添加的,而「任务3」和「任务4」所代表的then,都是和前面的then紧密相连,它们是一个整体,所以后来添加的scheduleMicrotask最后执行,即「任务2」最后才打印

总结

无论是事件队列还是微任务队列,都遵循3个原则

  1. 【微任务事件】级别高于【事件队列】

  2. 对于队列里添加新队列,新队列要在原先队列执行完之后才执行

  3. 对于同一类型的队列任务,先进先出

案例一:

先后添加【事件队列1】、【事件队列2】,【事件队列1】里面添加了一个【微任务事件1】,
`执行顺序`是【事件队列1】-> 【微任务事件1】-> 【事件队列2】(遵循原则123

案例二:

先后添加【事件队列1】、【事件队列2】,【事件队列1】里面添加了一个【事件队列3】,
`执行顺序`是【事件队列1】-> 【事件队列2】-> 【事件队列3】(遵循原则23

案例三:

先后添加【事件队列1】、【事件队列2】,【事件队列1】里面添加了一个【事件队列3】和一个【微任务事件1】,
`执行顺序`是【事件队列1】-> 【微任务事件1】-> 【事件队列2】-> 【事件队列3】(遵循原则123

多线程Isolate

我们知道Dart是单线程语言,但有时候难免会需要用到多线程的功能,为解决这个困扰,Dart提供了Isolate替代多线程

Isolate有【隔离】的意思,即多个Isolate之间不共享内存,每个Isolate有各自的存储空间,也就是说Dart的并发实际上是通过运行多个Isolate产生的结果。

Isolate初体验

来段代码体验下:

int a = 10;
getData() async {
  a++;
  print('line230 = $a');
  sleep(const Duration(seconds: 2));
  Isolate.spawn(func, 'message');
  sleep(const Duration(seconds: 2));
  print('line234 = $a');
}

void func(String message) {
  print('line238 --- $a');
  a = a + 3;
}

运行效果:

image.png

由此可见,Isolate里拿到的数据确实不受外部Dart代码影响。

Isolate修改外部值

前面,我们得知了Isolate的内存是独立的、不受外部影响的,这也导致了Isolate里面修改的内容,不能及时同步到外面。那么有什么办法可以解决这个问题呢?还真有,贴心的Dart为我们提供了ReceivePort

上代码:

int a = 10;
getData() async {
  a++;
  print('line246 = $a');
  ReceivePort port = ReceivePort();
  sleep(const Duration(seconds: 2));
  Isolate sort = await Isolate.spawn(func, port.sendPort);
  port.listen((message) {
    print('line251 = $a');
    a = message;
    print('line253 = $a');

  });
  sleep(const Duration(seconds: 2));
  print('line257 = $a');
  port.close();// 关闭窗口
  sort.kill();// 回收Isolate内存
}

void func(SendPort sendPort) {
  print('line261 --- $a');
  a = a + 3;
  sendPort.send(a);
}

image.png

  • line251打印,证明a的确是外部的,不再属于Isolate内部的;
  • line253打印,证明的确可以获得Isolate里面的内容。

最后记得关闭窗口、回收Isolate开辟的内存

compute初体验

compute是Isolate的上层封装,所以可以直接用computeResult接收数据

import 'dart:io';
import 'package:flutter/foundation.dart';

void main() {
  getData();
}

/// compute初体验
int a = 10;
getData() async {
  a++;
  print('line16 = $a');
  int computeResult = await compute(func2, '20');
  sleep(const Duration(seconds: 2));
  print('line19 = $a');
  print('line20 = $computeResult');
}

int func2(String message) {
  print('line24 --- $a');
  a = a + 3;
  return a;
}

运行效果:

IMG_5379.GIF

因为加了await,所以后面的代码要等前面执行完了才能继续执行。

compute和Future结合使用

有这么两段代码

  • 代码一
void main() => getData();

/// compute和Future结合使用
getData(){

  Future((){
    compute(func3,'20');
  }).then((value) { print('任务A结束'); });

  Future((){
    compute(func3,'30');
  }).then((value) { print('任务B结束'); });

  Future((){
    compute(func3,'40');
  }).then((value) { print('任务C结束');});

  Future((){
    compute(func3,'50');
  }).then((value) { print('任务D结束');});
}

void func3(String char){}
  • 代码二
void main() => getData();

getData(){

  Future((){
    return compute(func3,'20');
  }).then((value) { print('任务A结束'); });

  Future((){
    return compute(func3,'30');
  }).then((value) { print('任务B结束'); });

  Future((){
    return compute(func3,'40');
  }).then((value) { print('任务C结束');});

  Future((){
    return compute(func3,'50');
  }).then((value) { print('任务D结束');});
}

void func3(String char){}

执行的结果是

  • 任务一固定顺序打印:任务A结束、任务B结束、任务C结束、任务D结束
  • 任务二的打印是无需的

我们知道,多个Future任务的时候,Future之间是同步的,为什么到了任务二就变成异步打印了呢?

原因在于:Future里的return,会把当前函数的结果传给于then,而compute会创建一个子线程,所以then的打印实际上也在子线程,不同的子线程打印当然是无序的。