Dart中的异步编程和并发编程

´--- theme: channing-cyan

EventLoop

首先应该注意,Dart是单线程语言

Dart中的事件循环(Event Loop)

Dart 是一种单线程编程语言。如果某个操作阻塞了 Dart 线程,则应用程序在该操作完成之前没有任何进展。因此,为了可扩展性,没有 I/O 操作阻塞是至关重要的。dart:io 不是阻塞 I/O 操作,而是使用受 node.js、 EventMachine和 Twisted 启发的异步编程模型。

以上内容引用自这里

一般情况下实现异步的方式是多线程,另一种就是异步编程模型(Event Loop)

Event Loop

以下面这串代码为例,看下是如何实现异步的

void main() {
  print('before getIP');
  getIP().then((value) {
    print(value);
  });
  print('after getIP');
}
复制代码

getIP()返回的是一个Future,用于获取本机ip地址的网络请求; 首先执行print('before getIP');语句,等到getIP()时,dart首先会将getIP()方法返回的Future记录在一个内部数组中(此时Future的状态=incomplete),接着会把getIP()内部执行的代码推入事件队列,并且执行下一步,也就是print('after getIP');;然后检查事件队列的任务继续执行,当 getIP()执行完毕后会通过注册的回调,也就是then返回value,所以耗时事件不会阻塞循环,而在耗时事件之后的事件也就有机会被执行。

注意: 这里针对的是等待任务(比如I/O、网络请求),如果是计算密集型任务则应当尽可能利用处理器的多核,实现并行计算。

Event Loop中的两个队列

上面提到的事件队列指的是:Event QueueMicrotask Queue

  • Event Queue
    • 外部事件:
      • I/O
      • 手势
      • 绘图
      • 计时器
      • Steam
      • ...
    • 内部事件:
      • Future
  • Microtask Queue
    • 用于非常简短且需要异步执行的内部动作

不过大多数情况我们都不会用到Microtask Queue,该队列是交给Dart自己处理的。 而Microtask Queue优先级大于Event Queue

image.png

添加事件到Microtask QueueEvent Queue
  • 可以直接运行(没有添加到任何队列)
    • Future.sync()
    • Future.value()
    • _.then()
  • Microtask Queue
    • scheduleMicrotask()
    • Future.microtask()
    • _complete.then()
  • Event Queue
    • Future()
    • Future.delayed()
    • Timer()
特别强调:

_.then():表示在没有完成的Future中使用.then,这种情况下不会添加到任何队列,而是在Future执行完毕后立即执行

_complete.then():表示在已经完成的Future中使用.then,会将添加到Microtask Queue

Future.delayed():表示在延迟Duration后添加到Event QueueFuture.delayed(Duration(seconds: 0),())也会添加到Event Queue

Timer():Future内部的实现正是Timer

其实关于.then()的解释,源码注释中已经给了足够的说明

If this future is already completed, the callback will not be called,immediately, but will be scheduled in a later microtask.

测试_complete.then()加入Microtask Queue的情况
void main() {
  scheduleMicrotask(() => print('Microtask 1'));
  Future.microtask(() => print('Microtask 2'));

  //由于Future.value()已经执行完成,所以.then也应该尽快完成,因此需要加入到优先级高到Microtask Queue中
  Future.value(1).then((value) => print('Microtask 3'));
  print('main 1');
}
复制代码

打印结果⬇:

flutter: main 1
flutter: Microtask 1
flutter: Microtask 2
flutter: Microtask 3
复制代码
测试_.then()立即执行的情况
void main() {
  //如果.then加入任务队列,打印顺序为delayed -> then 1 -> Microtask -> then 2
  //如果.then不加入任务队列,则需要立即执行,打印顺序为delayed -> then 1 -> then 2 -> Microtask
  Future.delayed(Duration(seconds: 1),() => print('delayed'))
  .then((value) {
    scheduleMicrotask(() => print('Microtask'));
    print('then 1');
  })
  .then((value) => print('then 2'));
  print('main 1');
}
复制代码

打印结果⬇:

flutter: main 1
flutter: delayed
flutter: then 1
flutter: then 2
flutter: Microtask
复制代码

因此结论正确

Future

Future 是一个异步执行并且在未来的某一个时刻完成(或失败)的任务

当你实例化一个 Future 时:

  • 该 Future 的一个实例被创建并记录在由 Dart 管理的内部数组中;
  • 需要由此 Future 执行的代码直接推送到 Event 队列中去;
  • 该 future 实例 返回一个状态(= incomplete);
  • 如果存在下一个同步代码,执行它(非 Future 的执行代码

只要事件循环从 Event Loop 中获取它,被 Future 引用的代码将像其他任何 Event 一样执行。

当该代码将被执行并将完成(或失败)时,then()  或 catchError()  方法将直接被触发。 需要记住一些非常重要的事情:

Future 并非并行执行,而是遵循事件循环处理事件的顺序规则执行。

Async

当你使用Async关键字作为方法生命的后缀时,Dart会将其理解为:

  • 该方法返回值是一个Future
  • 它同步执行该方法直到第一个await关键字,然后它暂停该方法其他部分的执行
  • 一旦由await关键字引用的Future执行完成,下一行代码将立即执行

了解这一点是非常重要的,因为很多开发者认为 await 暂停了整个流程直到它执行完成,但事实并非如此。他们忘记了Event Loop的运作模式……

注意:await在同一个Future下生效。比如⬇️

//生效,await后面的代码会等待
void test() async {
  methodA();
  await methodB();
  await methodC();
}

//三个await没有关系,因为没有在同一个Future,
Future(() async {
  await Future.delayed(Duration(seconds: 1));
  print('A');
});

Future(() async {
  await Future.delayed(Duration(seconds: 1));
  print('B');
});

Future(() async {
  await Future.delayed(Duration(seconds: 1));
  print('C');
});
复制代码

另外,也需要谨记

async 并非并行执行,也是遵循事件循环处理事件的顺序规则执行。

执行method1()和method2()结果分别是什么?:

method1() {
  List<String> myArray = <String>['a','b','c'];
  print('before loop');
  myArray.forEach((value) async {
    await delayedPrint(value);
  });
  print('end of loop');
}

method2() async {
  List<String> myArray = <String>['a','b','c'];
  print('before loop');

  for(int i=0; i<myArray.length; i++) {
    await delayedPrint(myArray[i]);
  }
  print('end of loop');
}

Future<void> delayedPrint(String value) async {
  await Future.delayed(Duration(seconds: 1));
  print('delayedPrint: $value');
}
复制代码
method1() method2()
1. before loop 1. before loop
2. end of loop 2. delayedPrint: a (after 1 second)
3. delayedPrint: a (after 1 second) 3. delayedPrint: b (1 second later)
4. delayedPrint: b (directly after) 4. delayedPrint: c (1 second later)
5. delayedPrint: c (directly after) 5. end of loop (right after)

method1:使用 forEach()  函数来遍历数组。每次迭代时,它都会调用一个被标记为 async(因此是一个 Future)的新回调函数。执行该回调直到遇到 await,而后将剩余的代码推送到 Event 队列。一旦迭代完成,它就会执行下一个语句:“print(‘end of loop’)”。执行完成后,事件循环 将处理已注册的 3 个回调。

method2:所有的内容都运行在一个相同的代码「块」中,因此能够一行一行按照顺序执行。

Isolate

Dart是单线程语言,难道说我们是无法使用并发编程的吗, 答案当然是否定的,

Dart是单线程语言,但是可以使用Isolate实现并发编程, 对于Isolate的官方解释是:

independent workers that are similar to threads but don't share memory(类似于线程但不共享内存的独立工作者)

这里并没有把Isolate直接叫做线程,而是用类似于线程表示。

但是我们可以这样理解,

在Dart中每个线程是被封装在Isolate中,各线程间不共享内存,避免了dead lock的问题,由于线程独立,垃圾回收机制也非常高效。不同Isolate之间通过消息进行通信。

关系如下图 image.png

每个Isolate都有自己Event Loop(事件循环)

每个「Isolate」都拥有自己的「事件循环」及队列(MicroTask 和 Event)。这意味着在一个 Isolate 中运行的代码与另外一个 Isolate 不存在任何关联。

启动一个Isolate

1.用底层实现

需要自己建立通信,自己管理新建的Isolate和生命周期,自由度较高,但使用相对麻烦

由于Isolate之间不共享内存,因此,我们需要找到一种方法在调用者isolate与被被调用者isolate之间建立通信。

每个 Isolate 都暴露了一个将消息传递给另一个Isolate 的被称为SendPort的端口。两个isolate都需要互相知道对方的sendPort才可以通信

如下代码中

  • callerReceivePort.sendPort:调用者的端口
  • newIsolateSendPort.sendPort:被调用者的端口
  1. 创建Isolate并建立通信
  2. 发送消息
  3. 销毁Isolate
void main() async {

  //1.创建Isolate并建立通信
  //本地临时ReceivePort,用于检索新的isolate的SendPort
  ReceivePort callerReceivePort = ReceivePort();
  Isolate newIsolate = await createAndCommunication(callerReceivePort);

  //新的isolate的SendPort
  SendPort newIsolatePort = await callerReceivePort.first;

  //2.发送消息
  int result = await sendMessage(newIsolatePort, 10000000000);
  print(result);

  //3.释放Isolate
  disposeIsolate(newIsolate);
}

Future<Isolate> createAndCommunication(ReceivePort callerReceivePort) async {
  //初始化新的isolate(spawn():第一个参数是新isolate的入口方法,第二个参数是调用者的的SendPort)
  Isolate isolate = await Isolate.spawn(newIsolateEntry, callerReceivePort.sendPort);
  return isolate;
}

sendMessage(SendPort newIsolateSendPort, int num) async {
  //创建一个临时端口来接受回复
  ReceivePort responsePort = ReceivePort();
  //向调用者提供此isolate的SendPort
  newIsolateSendPort.send([responsePort.sendPort, num]);
  //等待回复并返回
  return responsePort.first;
}

void disposeIsolate(Isolate newIsolate) {
  newIsolate.kill(priority: Isolate.immediate);
  newIsolate = null;
}

//新isolate的入口(注意:在这里的内存都属于新建的isolate)
newIsolateEntry(SendPort callerSendPort) async {
  //一个新的SendPort实例,用来接受来自调用者的消息
  ReceivePort newIsolateReceivePort = ReceivePort();

  //向调用者提供此isolate的SendPort(注意:到这里双方通讯建立完成)
  callerSendPort.send(newIsolateReceivePort.sendPort);

  //监听调用者isolate向新的isolate输入的消息,并处理计算返回数据
  //注意:这里是Isolate的主程序,在isolate的处理和计算都在这里进行
  newIsolateReceivePort.listen((message) {
    //处理计算
    SendPort port = message[0];
    int num = message[1];
    int even = countEven(num);

    //发送结果
    port.send(even);
  });
}

//计算偶数个数(模拟计算密集型任务)
countEven(int num) {
  int count = 0;
  for(var i=0;i<=num;i++){
    if(i%2==0){
      count ++;
    }
  }
  return count;
}
复制代码
2.一次性计算(compute)

直接传入方法和参数即可,内部自己管理Isolate,会在方法直接完成后直接释放Isolate,使用相对简单

  1. 产生一个 Isolate,
  2. 在该 isolate 上运行一个回调函数,并传递一些数据,
  3. 返回回调函数的处理结果,
  4. 回调执行后终止 Isolate。

特别注意

Platform-Channel 通信仅仅主 isolate 支持。该主 isolate 对应于应用启动时创建的 isolate

也就是说,通过编程创建的 isolate 实例,无法实现 Platform-Channel 通信, 还有另一个解决办法 ->链接

参考资料

github.com/xitu/gold-m…

www.bilibili.com/video/BV12K…

juejin.cn/post/706514…

猜你喜欢

转载自juejin.im/post/7067415417194545188