阅读 729

Flutter - Dart事件循环机制与异步

欢迎关注微信公众号:FSA全栈行动 👋

一、Dart 异步

与 JavaScript 一样, Dart 是基于 事件循环机制单线程模型, 所以 Dart 中没有多线程, 也就没有主线程与子线程之分.

1、同步与异步

  • 同步: 同一线程中, 按照代码的编写顺序, 自上而下依次执行 (直观感受: 需要等待)
  • 异步: 代码执行中, 某段代码的执行不会影响后面代码的执行 (直观感受: 无需等待)

2、单线程模型

单线程模型:

  • 一条执行线上, 同时且只能执行一个任务(事件), 其他任务都必须在后面排队等待被执行.
  • 为了不阻碍代码的执行, 每遇到的耗时任务都会被挂起放入任务队列, 待执行结束后再按放入顺序依次执行队列上的任务, 从而达到异步效果.

单线程模型 与 多线程 各自的优势:

  • 单线程模型的优势: 避免了多线程的缺点, 比较适合于需要等待对方传送数据或返回结果的耗时操作, 如网络请求, IO 操作等.
  • 多线程的优势: 尽可能利用处理器的多核实现并行计算的计算密集型操作.

多线程 的缺点:

  • 会带来额外的资源和性能消耗.
  • 多个线程操作共享内存时需要加锁控制, 锁竞争会降低性能和效率, 复杂情况下还容易造成死锁.

3、事件循环机制

对于用户点击, 滑动, 硬盘 IO 访问等事件, 你不知道何时发生或以什么顺序发生, 所以得有一个永不停歇且不能阻塞的循环来等待处理这些 "突发" 事件. 于是, 基于 事件循环机制单线程模型 就出现了:

Dart 事件循环机制由 一个消息循环(Event Looper) 和 两个消息队列(Event Queue) 构成, 这两个消息队列分别是: 事件队列(Event queue)微任务队列(MicroTask queue).

Event Looper

Dart 在执行完 main 函数后, Event Looper 就开始工作, Event Looper 优先全部执行完 Microtask Queue 中的 event, 直到 Microtask Queue 为空时, 才会执行 Event Looper 中的 event, Event Looper 为空时才可以退出循环.

注意: Event Looper 为空时, 是 可以 而不是 一定 要退出, 视场景而定.

Event Queue

Event Queue 的 event 来源于 外部事件Future

  • 外部事件: 例如输入/输出, 手势, 绘制, 计时器, Stream 等
  • Future: 用于自定义 Event Queue 事件

对于外部事件, 一旦没有任何 microtask 要执行, Event loop才会考虑 event queue中的第一项,并且将会执行它.

通过 Future 实例向 Event Queue 添加事件:

Future(() {
  // 事件任务
});
复制代码

Microtask Queue

  • Microtask Queue 的优先级高于 Event Queue.
  • 使用场景: 想要在稍后完成一些任务(microtask) 但又希望在执行下一个事件(event)之前执行.

Microtask 一般用于非常短的内部异步动作, 并且任务量非常少, 如果微任务非常多, 就会造成 Event Queue 排不上队, 会阻塞 Event Queue 的执行(如: 用户点击没有反应). 所以, 大多数情况下优先考虑使用 Event Queue, 整个 Flutter 源代码仅引用 scheduleMicroTask() 方法 7 次.

通过 scheduleMicroTask() 函数向 Microtask Queue 添加任务:

scheduleMicrotask(() {
  // 微任务
});
复制代码

二、Future

Dart 中的异步操作主要使用 Futureasync/await, 整体与前端 ES6 中的 Promise, async/await 的使用差不多, 可以把 Future 理解为是一个自带 callback 效果的类.

1、基本使用

通过查看 Future 的构造函数知道, 创建时需要传入一个返回值类型是 FutureOr<T> 的函数:

factory Future(FutureOr<T> computation()) {
  ...
}
复制代码

这个 FutureOr<T> 是一个联合类型, 最终类型可能是 Future 或是泛型 T 的具体类型. 当不指定泛型 T 时, 实例类型为 Future<dynamic>. 下面是一个模拟网络耗时请求的例子:

Future<String> getNetworkData() {
  // 1. 将耗时操作包裹到Future的回调函数中
  return Future<String>(() {
    sleep(Duration(seconds: 2));

    return "Hello lqr"; // 只要有返回结果, 那么就执行Future对应的then的回调(相当于Promise-resolve)
    // throw Exception("error"); // 如果没有结果返回(有错误信息), 需要在Future回调中抛出一个异常(相当于Promise-reject)
  });
}
复制代码

Future 实例有 3 个常用方法:

  • then((value){...}): 正常运行时执行
  • catchError((err){...}): 出现错误时执行
  • whenComplete((){...}): 不管成功与否都会执行

通过以上 3 个方法, 即可获得 Future 实例的执行状况与结果:

main(List<String> args) {
  print("main start");

  // 2. 拿到结果
  var future = getNetworkData();
  future.then((value) => print(value)) // Hello lqr
      .catchError((err) => print(err))
      .whenComplete(() => print("执行完成")); // 不管成功与否都会执行

  print("main end");
}
复制代码

日志输出如下:

main start
main end

// 2秒后输出:
Hello lqr
执行完成
复制代码

注意, 以上 3 个方法是可以分开写的, 但每次执行完一个方法时需要对 future 实例重新赋值(相当于包了一层), 否则后续方法无效:

var future = getNetworkData();

// 错误写法:
future.then((value) => print(value));
future.catchError((error) => print(error)); // 无效

// 正确写法:
future = future.then((value) {
  print(value);
  return value;
});
future.catchError((error) => print(error)); // 有效
复制代码

2、链式调用

Future 可以在 then()方法中返回另一个 Future 实例, 从而达到链式调用的效果, 这对那些有数据关联的网络请求很有用:

main(List<String> args) {
  print("main start");

  // 链式调用, 执行多个数据处理
  Future(() {
    return "第一次结果";
  }).then((value) {
    print(value);
    return "第二次结果";
  }).then((value) {
    print(value);
    return "第三次结果";
  }).then((value) {
    print(value);
  }).catchError((error) {
    print(error);
  });

  print("main end");
}
复制代码

强调: Future 构造函数要求传入一个返回值类型是 FutureOr<T> 的函数, 但因为 FutureOr<T> 是联合类型, 所以, 这里可以返回另一个 Future 实例, 或者是一个具体类型数据, 比如字符串.

3、其它 API

Future 除了默认构造器外, 还提供了几个常用的命名构造器:

  • Future.value(): 创建一个返回具体数据的 Future 实例
  • Future.error(): 创建一个返回错误的 Future 实例
  • Future.delayed(): 创建一个延时执行的 Future 实例
main(List<String> args) {
  print("main start");

  Future.value("Hello lqr").then((value) => print(value));

  Future.error("出错了").catchError((error) => print(error));

  Future.delayed(Duration(seconds: 3))
      .then((value) => "Hello lqr")
      .then((value) => print(value));

  Future.delayed(Duration(seconds: 2), () => "Hello lqr")
      .then((value) => print("welcome"))
      .then((value) => throw Exception("出错了"))
      .catchError((error) => print(error))
      .whenComplete(() => print("执行完成")); // 不管成功与否都会执行

  print("main end");
}
复制代码

三、async/await

async/await 是 Dart 提供的可以用 同步的代码格式 实现 异步的调用过程语法糖.

1、基本使用

  • await 必须在 async 函数中使用
  • async 函数返回的结果必须是一个 Future
Future getNetworkData() async {
  var userId = await getUserId();
  var userInfo = await getUserInfo(userId);
  return userInfo.username // 会自动包裹成Future
}
复制代码

如果不使用 async/await, 那么上面的代码则需要这么写:

Future getNetworkData() {
  return getUserId().then((userId) {
    return getUserInfo(userId);
  }).then((userInfo) {
    return userInfo.username;
  });
}
复制代码

相比之下, 使用 async/await 写出来的代码在理解上会更加清晰.

四、isolate

所有的 Dart 代码都是在 isolate 中运行的, 它就是机器上的一个小空间, 具有自己的私有内存块和一个运行着 Event Looper 的单个线程. 每个 isolate 都是相互隔离的, 并不像线程那样可以共享内存. 一般情况下, 一个 Dart 应用只会在一个 isolate 中运行所有代码, 但如果有特殊需要, 可以开启多个:

注意: Dart 中没有线程的概念, 只有 isolate .

1、创建 isolate (Dart API)

Dart 默认提供了 Isolate.spawn(entryPoint, message) 用于开启 isolate, 通过源码可以知道形参 message 其实是 形参 entryPoint 对应的函数执行时需要的参数:

external static Future<Isolate> spawn<T>(
    void entryPoint(T message), T message,
    {bool paused = false,
    bool errorsAreFatal = true,
    SendPort? onExit,
    SendPort? onError,
    @Since("2.3") String? debugName});
复制代码

使用 Isolate.spawn(entryPoint, message) 开启 isolate, 并指定要执行的任务:

import 'dart:isolate';

main(List<String> args) {
  print("main start");

  Isolate.spawn(calc, 100);

  print("main end");
}

void calc(int count) {
  var total = 0;
  for (var i = 0; i < count; i++) {
    total += i;
  }
  print(total);
}
复制代码

2、isolate 通信 (单向)

isolate 间可以一起工作的唯一方法是通过来回传递消息. 一般情况下, 子isolate 会将运行结果通过管道以消息的形式发送到 主isolate, 并在 主isolateEvent Looper 中处理该消息, 这时就需要借助 ReceivePort 来处理消息的传递了:

  • 在启动 子isolate 时, 将 主isolate 的发送管道(SendPort)作为参数传递给 子isolate.
  • 子isolate 在执行完毕时, 可以利用管道(SendPort)给 主isolate 发送信息.
import 'dart:isolate';

main(List<String> args) async {
  print("main start");

  // 1. 创建管道
  var receivePort = ReceivePort();

  // 2. 创建isolate
  Isolate isolate = await Isolate.spawn(foo, receivePort.sendPort);

  // 3. 监听管道
  receivePort.listen((message) {
    print(message);
    // 不再使用时, 关闭管道
    receivePort.close();
    // 不再使用时, 将 isolate 杀死
    isolate.kill();
  });

  print("main end");
}

void foo(SendPort sendPort) {
  sendPort.send("Hello lqr");
}
复制代码

以上只实现了 isolate 的单向通信, 双向通信比较麻烦, 有兴趣可以再查看一些其他资料.

3、创建 isolate (Flutter API)

Flutter 提供了更为方便的开启 isolate 的 API: compute() 函数. 以下是示例代码:

main(List<String> args) async {
  int result = await compute(powerNum, 5);
  print(result);
}

int powerNum(int num) {
  return num * num;
}
复制代码

compute() 是 Flutter 的 API, 不是 Dart 的 API, 所以, 上面的代码只能在 Flutter 项目中才能运行.

参考资料

文章分类
Android
文章标签